原创 谢志春(志春) 淘系技术 2020-09-14
上手
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() 代表任意类型对象
-
anyString() 代表任意字符串
@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 的调用次数校验:
owerMockito.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 部署了。
艾玛,真香~