淘系的技术发展已经有相当一段历史了,在历史的长河中总能沉淀出很多复杂的巨型项目,包罗多个业务,而且往往服务依赖比较复杂;再加上一些特殊环境变量的设置,想要在本地运行、debug 自测这种大型应用的难度越来越高;尤其是对环境不太熟悉的新人而言成本会更高。
这类应用的单元测试不能像微服务化的应用一样,可以方便的将整个 service 在本地 Run Test,但是依靠于日常开发部署环境的远程 debug、日志、Arthas 等工具定位项目自测联调中的问题又会显得格外的笨重,问题修复几秒钟,发布一次 10min 会成为严重的效率瓶颈。
如何高效的自测代码逻辑,如何不启动整个服务就能验证我的目标方法呢?那就是我今天要介绍的三板斧 Mockito + PowerMock + AssertJ
上手
Mock 框架能帮助我们 mock 待测试的类中使用到的外部服务依赖,分布式缓存,DB查询等复杂逻辑,让我们轻松验证待测试类的目标方法的逻辑,当遇到外部依赖时可通过存根 mock 对应的返回结果,从而专注于验证本方法的逻辑正确性,而且跑单元测试不用把整个项目在本地跑起来,只会把当前测试所用到的类加载出来。换言之,Mock 能让代码对外部系统(或复杂依赖)隔离,不需要进行各种初始化操作。在假设外部依赖都能如预期返回的情况下验证自身逻辑的自洽性。
talk is cheap,show me your code. 开始盘它~
▐ 配置 Maven 依赖
-
<dependency>
-
<groupId>junit
</groupId>
-
<artifactId>junit
</artifactId>
-
<version>4.11
</version>
-
<scope>test
</scope>
-
</dependency>
-
<dependency>
-
<groupId>org.mockito
</groupId>
-
<artifactId>mockito-core
</artifactId>
-
<version>3.5.2
</version>
-
<scope>test
</scope>
-
</dependency>
-
<dependency>
-
<groupId>org.powermock
</groupId>
-
<artifactId>powermock-module-junit4
</artifactId>
-
<version>2.0.5
</version>
-
<scope>test
</scope>
-
</dependency>
-
<dependency>
-
<groupId>org.powermock
</groupId>
-
<artifactId>powermock-api-mockito2
</artifactId>
-
<version>2.0.5
</version>
-
<scope>test
</scope>
-
</dependency>
-
<dependency>
-
<groupId>org.assertj
</groupId>
-
<artifactId>assertj-core
</artifactId>
-
<!-- use 2.9.1 for Java 7 projects -->
-
<version>3.17.1
</version>
-
<scope>test
</scope>
-
</dependency>
▐ Mockito
Mockito 可以 mock 类的 public 方法或接口的方法。它是通过 cglib 动态生成一个 Proxy,因此在未指定某个方法行为的情况下,会默认返回空值,当然,一个完善的框架肯定会支持直接访问被代理的对象的真实方法的,下文会有介绍,一共会有3种方式哦,我们继续吧。
这里我们使用的 mock 类定义如下:
-
import java.util.concurrent.TimeUnit;
-
-
-
public
class
MockTarget {
-
-
-
public void soSth() {
-
System.
out.println(
"do sth.");
-
}
-
-
-
public String sayHello() {
-
return
"Hello";
-
}
-
-
-
public String sayHello(String greetings) {
-
return
"Hello " + greetings;
-
}
-
-
-
public String callMethod(Object p) {
-
return
"callMethod " + p.toString();
-
}
-
-
-
public String callMethodWait(long million) {
-
try {
-
TimeUnit.MILLISECONDS.sleep(million);
-
}
catch (InterruptedException ignored) {
-
}
-
return
"callMethod sleep " + million;
-
}
-
-
-
public Object callMethodWithException(Object p) {
-
throw
new IllegalStateException(
"测试异常");
-
}
-
}
when..then
用于 mock 方法调用的各种返回情况。
通过 doCallRealMethod 指定 mock 对象的方法调用它的真实逻辑,也可通过 thenAnswer(Answers.CALLS_REAL_METHODS) 实现
通过 when..thenThrow 或者 doThrow..when 的方式 mock 目标方法返回对应的异常
通过 AssertJ 的句法 assertThatExceptionOfType..isThrownBy..withXxx断言某个方法的执行会抛出预期异常
anyXxx() 可用于表示任意类型的任意参数
-
anyString() 代表任意字符串
anyInt() 代表任意int数值
anyObject() 代表任意类型对象
-
@Test
-
public void testWhenAndThen() {
-
MockTarget mock = Mockito.mock(MockTarget.
class);
-
when(mock.sayHello()).thenReturn(
"mock hello");
-
assertEquals(mock.sayHello(),
"mock hello");
-
-
-
doCallRealMethod().
when(mock).sayHello();
-
assertEquals(mock.sayHello(),
"Hello");
-
-
-
when(mock.sayHello(anyString())).thenAnswer(Answers.CALLS_REAL_METHODS);
-
assertEquals(mock.sayHello(
"testRun"),
"Hello testRun");
-
-
-
when(mock.callMethod(any())).thenReturn(
"mock return");
-
assertEquals(mock.callMethod(new Object()),
"mock return");
-
-
-
when(mock.callMethodWithException(any())).thenThrow(new RuntimeException(
"mock throw exception"), new IllegalArgumentException(
"test illegal argument"));
-
-
-
Assertions.assertThatExceptionOfType(RuntimeException.
class)
-
.isThrownBy(() -> mock.callMethodWithException(
"first invoke"))
-
.withMessage(
"mock throw exception");
-
Assertions.assertThatExceptionOfType(IllegalArgumentException.
class)
-
.isThrownBy(() -> mock.callMethodWithException(
"second invoke"))
-
.withMessage(
"test illegal argument")
-
.withNoCause();
-
-
-
doAnswer((Answer<String>) invocation -> {
-
Object[] args = invocation.getArguments();
-
MockTarget mock1 = (MockTarget) invocation.getMock();
-
return
"mock sayHello " + args[
0];
-
}).
when(mock).sayHello(
"doAnswer");
-
assertEquals(mock.sayHello(
"doAnswer"),
"mock sayHello doAnswer");
-
-
-
// 1.doNothing, 2. throw RuntimeException
-
doNothing().doThrow(RuntimeException.
class).
when(mock).soSth();
-
mock.soSth();
-
Assertions.assertThatExceptionOfType(RuntimeException.
class).isThrownBy(mock::soSth);
-
}
verify
用于验证某个方法是否被调用,包括可以验证该方法被调用的次数,以及等待异步方法调用完成等特性。
常用句式 verify(mockObject [, times(n) ] ).targetMethod
-
@Test
-
public void testVerifyInteractions() {
-
// mock creation
-
List mockedList = mock(List.class);
-
mockedList.clear();
-
//
only clear() invoked
-
verify(
mockedList, only()).
clear(
);
-
verifyNoMoreInteractions(mockedList);
-
-
-
// 此处不会抛异常,因为是mock的list对象,非实际list对象
-
when(mockedList.
get(
1)).thenReturn(
"two");
-
assertEquals(mockedList.
get(
1),
"two");
-
-
-
// using mock object - it does not throw any "unexpected interaction" exception
-
mockedList.
add(
"one");
-
-
-
// selective, explicit, highly readable verification
-
verify(mockedList).
add(
"one");
-
verify(mockedList, times(
1)).clear();
-
verify(mockedList, atLeastOnce()).
add(
"one");
-
verify(mockedList, atMostOnce()).
add(
"one");
-
verify(mockedList, atMost(
1)).
add(
"one");
-
verify(mockedList, atLeast(
1)).
add(
"one");
-
verify(mockedList, never()).
add(
"never");
-
}
verify 之 after 与 timeout
针对异步调用,我们可以通过 after 或 timeout 等待一定时间,来校验目标方法是否有调用,以及在此之后获取目标方法的返回值,作进一步逻辑校验
after 会阻塞等满时间之后再往下执行,是固定等待多长时间的语义
timeout 在等待期内,拿到结果后立即向下执行,不做多余等待;是最多等待多长时间的语义
-
@Test
-
public void testAfterAndTimeout() throws Exception {
-
MockTarget mock = mockTarget;
-
doCallRealMethod().when(mock).callMethodWait(anyLong());
-
-
-
final
long timeout =
500L;
-
final
long delta =
100L;
-
// 异步调用
-
CompletableFuture<Void> async = CompletableFuture.runAsync(() -> {
-
try {
-
TimeUnit.MILLISECONDS.sleep(timeout);
-
}
catch (InterruptedException ignored) {
-
}
-
mock.sayHello();
-
mock.callMethod(
"test");
-
mock.callMethod(
"test");
-
});
-
-
-
// timeout() exits immediately with success when verification passes
-
// verify(mock, description("invoke not yet, This will print on failure")).callMethod("test");
-
verify(mock, timeout(timeout + delta).times(
2)).callMethod(
"test");
-
// immediately success
-
verify(mock, timeout(
10)).sayHello();
-
async.get();
-
-
-
// after() awaits full duration to check if verification passes
-
verify(mock, after(
10).times(
2)).callMethod(
"test");
-
verify(mock, after(
10)).sayHello();
-
}
spy
spy 的官方定义是:
partial mocking, real methods are invoked but still can be verified and stubbed
会调用被 spy 的真实对象的方法,但仍能被 Mockiton 所直接用于 mock 和 verify,也就是说在没有配置 mock 行为的情况下默认是调用被 mock 对象的真实方法。
句式 doXxx..when 当同一目标方法上定义了多个 mock 行为,后序 mock 可以覆盖前序 mock
clearInvocations 仅清理之前的调用
reset 会重置为初始状态(所有中途的赋值都会被清理掉)
-
@Test
-
public void testDoReturn() {
-
// real creation
-
List list =
new LinkedList();
-
List spy = spy(list);
-
-
-
//optionally, you can stub out some methods:
-
int mockSize =
100;
-
when(spy.size()).thenReturn(mockSize);
-
//size() method was stubbed - 100 is printed
-
assertEquals(spy.size(), mockSize);
-
-
-
// Overriding a previous exception-stubbing:
-
when(spy.size()).thenThrow(
new IllegalStateException(
"not init"));
-
doReturn(mockSize).
when(spy).size();
-
assertEquals(spy.size(), mockSize);
-
//Impossible: real method is called so spy.get(0) throws IndexOutOfBoundsException (the list is yet empty)
-
Assertions.assertThatExceptionOfType(IndexOutOfBoundsException.class).isThrownBy(() -> spy.
get(0));
-
doReturn("mock data").
when(spy).
get(1);
-
-
-
//
using the spy calls real methods
-
spy.
add("one");
-
assertEquals(spy.
get(0), "one");
-
-
-
/*
-
Use
this method
in order to only clear invocations,
when stubbing
is non-trivial. Use-cases can be:
-
You are
using a dependency injection framework to inject your mocks.
-
The mock
is used
in a stateful scenario. For example a class
is Singleton which depends
on your mock.
-
Try to avoid
this method at all costs. Only clear invocations
if you are unable to efficiently test your program.
-
*/
-
clearInvocations(spy);
-
verify(spy, times(0)).
add("two");
-
reset(spy);
-
when(spy.size()).thenReturn(0);
-
assertEquals(spy.size(), 0);
-
}
▐ PowerMock
以上介绍的是 Mockiton 中常用的API,而 PowerMock 则更强大,可以 mock static 方法,private 方法,final 方法,enum,构造函数调用等。
示例代码中用到的测试类如下:
-
public
enum TypeEnum {
-
Y(
"TRUE"),
-
N(
"FALSE");
-
-
-
private
final String title;
-
-
-
TypeEnum(String title) {
-
this.title = title;
-
}
-
-
-
public String getTitle() {
-
return title;
-
}
-
}
-
-
-
public
final
class FinalTarget {
-
-
-
public FinalTarget() { }
-
-
-
public final String finalMethod() {
-
return
"Hello final!";
-
}
-
-
-
}
-
-
-
public
class StaticTarget {
-
-
-
public static String firstMethod(String name) {
-
return
"Hello " + name +
" !";
-
}
-
-
-
public static String secondMethod() {
-
return
"Hello no one!";
-
}
-
-
-
}
-
-
-
public
class PartialTarget {
-
private String arg;
-
-
-
public PartialTarget(String arg) {
-
this.arg = arg;
-
}
-
-
-
public PartialTarget() { }
-
-
-
public String getArg() {
-
return arg;
-
}
-
-
-
private String privateWithArg(String arg) {
-
return
"Hello privateWithArg! " + arg;
-
}
-
-
-
public String privateMethodCaller(String arg) {
-
return privateWithArg(arg) +
" privateMethodCall.";
-
}
-
}
类注解
在使用 PowerMockito mock static , private , final , enum , constructor 之前需要在测试类上加入如下注解:
-
@RunWith(PowerMockRunner.class)
-
@PrepareForTest({
StaticTarget
.class,
PartialTarget
.class,
TypeEnum
.class,
FinalTarget
.class})
static
PowerMockito.mockStatic 声明了要 mock static 方法的类
-
PowerMockito
.mockStatic(
StaticTarget
.class);
-
StaticTarget
.firstMethod("
xxx");
verify
值得注意的是,它的 verify 方法使用比 Mockiton 更复杂。
需要先声明一下验证目标类的静态方法再紧接着调用一下,表示待验证的目标方法
-
PowerMockito.verifyStatic(StaticTarget.
class);
//
1
-
StaticTarget.firstMethod(invokeParam);
//
2
也有类似于 Mockiton 的调用次数校验:
-
PowerMockito
.verifyStatic(
StaticTarget
.class,
times(1));
-
PowerMockito
.verifyStatic(
StaticTarget
.class,
Mockito
.atLeastOnce());
private
PowerMock 模拟 private 方法 "privateWithArg" 的返回值并校验 "privateWithArg" 被调用的次数
-
PartialTarget partialMock = PowerMockito.mock(PartialTarget.
class);
-
doCallRealMethod().
when(partialMock).privateMethodCaller(anyString());
-
PowerMockito.doReturn(
"mockResult").
when(partialMock,
"privateWithArg", any());
-
// *privateMethodCaller* will invoke method *privateWithArg*
-
String result = partialMock.privateMethodCaller(
"arg");
-
Assert.assertEquals(result,
"mockResult privateMethodCall.");
-
PowerMockito.verifyPrivate(partialMock, times(
1)).invoke(
"privateWithArg",
"arg");
final
PowerMock 校验 mock final方法
-
FinalTarget finalTarget = PowerMockito.mock(FinalTarget.
class);
-
String finalReturn =
"finalReturn";
-
PowerMockito.
when(finalTarget.finalMethod()).thenReturn(finalReturn);
-
Assert.assertThat(finalTarget.finalMethod(),
is(finalReturn));
enum
PowerMock mock enum,这里的 Whitebox.setInternalState 可以设置 TypeEnum fieldName=N 的值为给定的 mock 枚举
-
String mockValue =
"mock title";
-
TypeEnum typeMock = PowerMockito.mock(TypeEnum.
class);
-
Whitebox.setInternalState(TypeEnum.
class,
"N", typeMock);
-
when(typeMock.getTitle()).thenReturn(mockValue);
-
Assert.assertEquals(TypeEnum.N.getTitle(), mockValue);
-
Assert.assertEquals(TypeEnum.Y.getTitle(),
"TRUE");
constructor
构造器 mock 与 verify
-
String arg =
"special arg";
-
PartialTarget partialWithArgSpy = PowerMockito.spy(
new PartialTarget(arg));
-
whenNew(PartialTarget.class).withNoArguments().thenReturn(partialWithArgSpy);
-
PartialTarget partialNoArg =
new PartialTarget();
-
Assert.assertEquals(partialNoArg.getArg(), arg);
-
verifyNew(PartialTarget.class).withNoArguments();
完整示例如下:
-
import org.assertj.core.api.Assertions;
-
import org.junit.Assert;
-
import org.junit.Test;
-
import org.junit.runner.RunWith;
-
import org.mockito.Mockito;
-
import org.powermock.api.mockito.PowerMockito;
-
import org.powermock.core.classloader.annotations.PrepareForTest;
-
import org.powermock.modules.junit4.PowerMockRunner;
-
import org.powermock.reflect.Whitebox;
-
import static org.hamcrest.core.Is.is;
-
import static org.mockito.ArgumentMatchers.anyString;
-
import static org.mockito.Mockito.times;
-
import static org.powermock.api.mockito.PowerMockito.doCallRealMethod;
-
import static org.powermock.api.mockito.PowerMockito.verifyNew;
-
import static org.powermock.api.mockito.PowerMockito.when;
-
import static org.powermock.api.mockito.PowerMockito.whenNew;
-
-
-
@
RunWith(
PowerMockRunner.
class)
-
@PrepareForTest({StaticTarget.
class, PartialTarget.
class, TypeEnum.
class, FinalTarget.
class})
-
public
class PowerMockTest {
-
-
-
@Test
-
public void testStatic()
throws Exception {
-
PowerMockito.mockStatic(StaticTarget.
class);
-
String mockResult = "Static mock";
-
PowerMockito.when(StaticTarget.firstMethod(anyString())).thenReturn(mockResult);
-
String invokeParam = "any String parameter";
-
Assert.assertEquals(StaticTarget.firstMethod(invokeParam), mockResult);
-
-
-
// Verification of a
static method
is done
in two steps.
-
PowerMockito.verifyStatic(StaticTarget.
class); // 1
-
// StaticTarget.secondMethod();// not invoked
-
StaticTarget.firstMethod(invokeParam);// 2
-
// use argument matchers
-
PowerMockito.verifyStatic(StaticTarget.
class); // 1
-
StaticTarget.firstMethod(anyString()); // 2
-
// atLeastOnce
-
PowerMockito.verifyStatic(StaticTarget.
class, Mockito.atLeastOnce()); // 1
-
StaticTarget.firstMethod(anyString()); // 2
-
// times
-
PowerMockito.verifyStatic(StaticTarget.
class, times(1)); // 1
-
StaticTarget.firstMethod(anyString()); // 2
-
-
-
// partial mocking of a
private method & verifyPrivate
-
// PartialTarget partialNoArgSpy = PowerMockito.spy(new PartialTarget());
-
PartialTarget partialMock = PowerMockito.mock(PartialTarget.
class);
-
doCallRealMethod().when(partialMock, "privateMethodCaller", anyString());
-
PowerMockito.doReturn("mockResult").when(partialMock, "privateWithArg", any());
-
// *privateMethodCaller* will invoke method *privateWithArg*
-
String result = partialMock.privateMethodCaller("arg");
-
Assert.assertEquals(result, "mockResult privateMethodCall.");
-
PowerMockito.verifyPrivate(partialMock, times(1)).invoke("privateWithArg", "arg");
-
-
-
// Final
-
FinalTarget finalTarget = PowerMockito.mock(FinalTarget.
class);
-
String finalReturn = "finalReturn";
-
PowerMockito.when(finalTarget.finalMethod()).thenReturn(finalReturn);
-
Assert.assertThat(finalTarget.finalMethod(),
is(finalReturn));
-
-
-
//
enum
-
String mockValue = "mock title";
-
TypeEnum typeMock = PowerMockito.mock(TypeEnum.
class);
-
Whitebox.setInternalState(TypeEnum.
class, "N", typeMock);
-
when(typeMock.getTitle()).thenReturn(mockValue);
-
Assert.assertEquals(TypeEnum.N.getTitle(), mockValue);
-
Assert.assertEquals(TypeEnum.Y.getTitle(), "TRUE");
-
-
-
// verify New
-
String arg = "special arg";
-
PartialTarget partialWithArgSpy = PowerMockito.spy(new PartialTarget(arg));
-
whenNew(PartialTarget.
class).withNoArguments().thenReturn(partialWithArgSpy);
-
PartialTarget partialNoArg = new PartialTarget();
-
Assert.assertEquals(partialNoArg.getArg(), arg);
-
verifyNew(PartialTarget.
class).withNoArguments();
-
-
-
//
throw exception
-
PowerMockito.doThrow(new ArrayStoreException("Mock secondMethod error")).when(StaticTarget.
class);
-
StaticTarget.secondMethod();
-
// AssertJ: Exception assertions
-
Assertions.assertThatThrownBy(StaticTarget::secondMethod)
-
.isInstanceOf(ArrayStoreException.
class)
-
.hasNoCause()
-
.hasMessage("Mock secondMethod error");
-
}
-
}
▐ AssertJ
上面提到的 AssertJ 是 Assert 的一些功能增强,以流式编程的方式调用,下面介绍一些常用的用法
isIn,isNotIn 和 matches 用于断言匹配条件
filteredOn 可以针对 assertThat 中传入的参数进行过滤,类似 java8 中Stream() 的 filter 方法
extracting 可以针对 assertThat 中传入的元组进行字段提取校验
assertThatExceptionOfType 和 assertThatThrownBy 可用于捕获预期的异常
为了方便使用,AssertJ 还提供了几种常用的异常断言的包装器:
-
// AssertJ provides wrappers for common exception types
-
Assertions.assertThatNoException();
-
Assertions.assertThatIOException();
-
Assertions.assertThatNullPointerException();
-
Assertions.assertThatIllegalStateException();
-
Assertions.assertThatIllegalArgumentException();
示例如下:
-
import org.assertj.core.api.Assertions;
-
import org.junit.Test;
-
import java.util.Arrays;
-
import java.util.List;
-
import static org.assertj.core.api.Assertions.tuple;
-
-
-
public
class AssertTest {
-
@
Test
-
public void testAssertJ() {
-
String title =
"foo";
-
AssertTarget assertTarget = new
AssertTarget(title,
12,
TypeEnum.
Y);
-
-
-
String msg =
"Illegal Argument error";
-
Exception cause = new
NullPointerException(
"cause exception msg");
-
Assertions.assertThatExceptionOfType(
IllegalArgumentException.
class)
-
.isThrownBy(() -> assertTarget.throwIllegalArgumentException(msg, cause))
-
.withMessage(msg)
-
.withMessageContaining("Argument error")
-
.overridingErrorMessage("new error message")
-
.withCause(cause);
-
-
-
Assertions.assertThatThrownBy(() -> assertTarget.throwIllegalArgumentException(msg, cause))
-
.isInstanceOf(IllegalArgumentException.
class)
-
.hasMessageContaining("Argument error");
-
-
-
Assertions.assertThat(assertTarget.getTitle())
-
//
as()
is used to describe the test and will be shown before the error message
-
.
as("PartialTarget's arg
is not match", assertTarget.getTitle())
-
.
startsWith(title)
-
.endsWith(title)
-
.
contains(title)
-
.isNotEqualTo("foo bar")
-
.isEqualToIgnoringCase("FOO")
-
.isEqualTo(title);
-
-
-
AssertTarget target1 = new AssertTarget("testTitle", 12, TypeEnum.N);
-
AssertTarget target2 = new AssertTarget("titleVal1", 16, TypeEnum.N);
-
AssertTarget target3 = new AssertTarget("titleVal2", 18, TypeEnum.Y);
-
AssertTarget target4 = new AssertTarget("titleVal3", 20, TypeEnum.N);
-
List<AssertTarget> assertTargetRing = Arrays.asList(target1, target2, target3);
-
-
-
Assertions.assertThat(target1.getNum()).withFailMessage("the num not matches").isEqualTo(12);
-
Assertions.assertThat(target1.getType().equals(TypeEnum.N)).isTrue();
-
Assertions.assertThat(target1).isIn(assertTargetRing);
-
Assertions.assertThat(target4).isNotIn(assertTargetRing);
-
Assertions.assertThat(target4).matches(e -> e.getNum() > 18 && e.getType().equals(TypeEnum.N));
-
-
-
Assertions.assertThat(assertTargetRing)
-
// extracting multiple values at once grouped
in tuples
-
.extracting("num", "type.title")
-
.
contains(tuple(16, TypeEnum.N.getTitle())
-
, tuple(18, TypeEnum.Y.getTitle()));
-
-
-
Assertions.assertThat(assertTargetRing)
-
// filtering a collection before asserting
-
.filteredOn(e -> e.getTitle().
startsWith("title"))
-
.extracting(AssertTarget::getNum)
-
.
contains(16, 18);
-
-
-
}
-
}
真香
以上针对自己使用的 mock 单元测试的三板斧 Mockito + PowerMock + AssertJ 常用姿势做了小结。
利用 Mockiton 做常规类和接口的 mock
PowerMock 则可以 mock 静态方法,私有方法,final 方法,枚举,构造函数等
AssertJ 流式风格,增强 assert 判断逻辑和校验异常流程
更多姿势等待大家在实操中继续解锁,利用这些姿势在后续的开发自测中可以更快速的做自我逻辑验证,而我再也不必等待每次项目开发环境的 10min 部署了。
艾玛,真香~
写在最后
最后的结尾,介绍一下我们团队吧,最近团队内新增了一条业务线,HC多多,机会多多,欢迎内转和外部投递:
淘系技术部-商家运营
我们是淘系商家运营中台团队,负责淘系千万级商家运营技术体系构建,在这里,你将负责攻克商家域复杂业务模型、海量数据挖掘、高稳定性等带来的技术难题与挑战,打造全市场商家运营技术架构标杆,驱动市场增量价值,并成为最懂商业的技术人。
我们HC不限,可以直接跟老板聊,招聘流程快。期待你的加入,共建To B端最具代表性的商业技术体系,迎接产业互联网的到来。
地点杭州阿里巴巴西溪园区。欢迎各路大侠加入!
简历投递邮箱????:xzc270316@alibaba-inc.com
参考文档:
Mockito: https://site.mockito.org
PowerMock: https://powermock.github.io
AssertJ: https://assertj.github.io/doc
✿ 拓展阅读
作者|谢志春(志春)
编辑|橙子君
出品|阿里巴巴新零售淘系技术
转载:https://blog.csdn.net/Taobaojishu/article/details/108591311