如果您有任何问题或想要讨论的话题,欢迎在评论区留言,我将尽快回复。
欢迎扫描下方二维码,阅读更多成体系的干货文章!
上一篇《单元测试》初步介绍了 Junit 写单元测试的使用方法,可以完成基本测试,但是有更复杂的测试场景的时候,比如外部接口 Restful 、RPC 接口调用、DB 调用,获取不容易构造/获取的对象,需要创建一个 mock 对象来模拟对象的行为时,就需要 Mock 框架帮我们实现了。
什么是 Mock ?
Mock的字面意思就是模仿,虚拟,在单元测试中,使用Mock可以虚拟出一个外部依赖对象,可以降低测试的复杂度,只关心当前单元测试的方法。常见的有 Mockito、EasyMock、Jmockit、PowerMock、Spock 等等,SpringBoot 默认的 Mock 框架是 Mockito。
什么是 Mockito ?
Mockito 是美味的 Java 单元测试 Mock 框架。Mockito 库能够 Mock 对象、验证结果以及打桩(stubbing)。 大多 Java Mock 库如 EasyMock 都是 expect-run-verify (期望-运行-验证)方式,而 Mockito 则使用更简单,更直观的方式:在执行后的互动中提问,Mockito 并不需要 “expectation(期望)”的概念,只有 stub 和验证,非 expect-run-verify 方式 意味着,Mockito 无需准备昂贵的前期启动。它目标是透明的,让开发人员专注于测试选定的行为。
Mockito 拥有非常少的 API,所以使用 Mockito 几乎没有时间成本,因为只有一种创造 mock 的方式。只要记住,在执行前 stub,而后在交互中验证,这样实现 TDD Java 代码会变得非常自然,更多信息参考 官网。
Mockito 入门
依赖
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>4.2.0</version>
</dependency>
<!-- 如果是SpringBoot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>xxx</version>
<scope>test</scope>
</dependency>
@Mock/@Spy/@InjectMocks/@Captor/@MockBean/@SpyBean
@Mock/@Spy:相当于 mock/spy 的快捷方式 @InjectMocks:注解的对象相对于 spy 对象,并且可以将 mock/spy 对应的对象自动注入此对象对应的属性中 @Captor:可以获取 Matcher 实际执行时对应的参数 @MockBean/@SpyBean:相对于 @Mock/@Spy,并且此注释的对象,被加入到 spring 容器中
注解生效
- 在测试类上使用:@RunWith(MockitoJUnitRunner.class)
- 在 @Before 中调用:MockitoAnnotations.initMocks(this)
- 在类中定义:@Rule public MockitoRule mockito = MockitoJUnit.rule()
静态导入会使代码更简洁
import static org.mockito.Mockito.*;
//创建mock对象,mock一个List接口
List mockedList = mock(List.class);
//如果不使用静态导入,则使用 Mockito 调用
List mockList = Mockito.mock(List.class);
mock/spy
mock
//可以mock具体类
List mockedList = mock(LinkedList.class);
//可以mock接口
List interfaceList = mock(List.class);
//以下两个等效,并且是默认值
mock(LinkedList.class,Mockito.RETURNS_DEFAULTS);
mock(LinkedList.class,withSettings().defaultAnswer(Mockito.RETURNS_DEFAULTS));
//默认返回一个 SmartNull,可记录更详细的日志信息,mockito 4.0 后为默认
mock(LinkedList.class,Mockito.RETURNS_SMART_NULLS);
//如果返回 null,且不是 final,就返回一个 mock,不建议使用
mock(LinkedList.class,Mockito.RETURNS_MOCKS);
//可以级联调 when(),不建议使用
mock(LinkedList.class,Mockito.RETURNS_DEEP_STUBS);
//默认调用真实方法,这个方法等效于 spy
mock(LinkedList.class,Mockito.CALLS_REAL_METHODS);
//适用于 Builder 模式,一般用不上
mock(LinkedList.class,Mockito.RETURNS_SELF);
spy
//等效于:mock(ListedList.class,Mockito.CALLS_REAL_METHODS);
spy(LinkedList.class);
List list = new ArrayList();
spy(list);
when/then
有几种形式
- doXxx() . when() 推荐
- when().doXxx() 不推荐(当调用真实方法时,会先直接 when 中的方法,这时的真实方法可能有问题,例如没有实现而报错)
- any():可能造成 NPE
@Test(expected = Exception.class)
public void testWhenThen() {
List list = mock(ArrayList.class);
//调用 list.get() 三次,依次返回 "first","second","third"
doReturn("first", "second", "third").when(list).get(anyInt());
System.out.println(list.get(0));
System.out.println(list.get(1));
System.out.println(list.get(2));
//当使用 matcher 时,所有参数都必须是 matcher 形式
doThrow(new RuntimeException(), new IllegalAccessError()).when(list).set(anyInt(), anyInt());
list.set(1, 1);
//调用真实方法
doCallRealMethod().when(list).get(0);
System.out.println(list.get(0));
doNothing().when(list).clear();
list.clear();
//doAnswer
doAnswer(invocation -> 11).when(list).get(1);
assert 11 == (int)list.get(1);
}
举例
验证某些行为
一旦创建 mock 将会记得所有的交互。你可以选择验证你感兴趣的任何交互。
@Test
public void testLinkedList() {
//mock
List mockedList = mock(LinkedList.class);
mockedList.add("one");
mockedList.clear();
// verification
verify(mockedList).add("one");
verify(mockedList).clear();
}
如何做一些测试桩
@Test
public void testLinkList() {
// mock
LinkedList mockedList = mock(LinkedList.class);
// 测试桩,当调用 mockList.get(0)的时候,返回 "first"
when(mockedList.get(0)).thenReturn("first");
//当调用mockList.get(1)的时候,抛出一个运行时异常
when(mockedList.get(1)).thenThrow(new RuntimeException());
// 打印 mockedList.get(0) 即 "first"
System.out.println(mockedList.get(0));
// 打印 mockedList.get(999) 即 null
System.out.println(mockedList.get(999));
}
小结:
- 默认情况下,所有方法都会返回值,一个 mock 要么返回默认值要么返回 null,例如,一个原始/基本类型的包装值或适当的空集, int/Integer 就是 0, boolean/Boolean 就是 false。
- Stubbing 可以被覆盖,一旦 stub,该方法将始终返回一个 stub 的值,无论它有多少次被调用。
参数匹配器
Mockito 验证参数值使用 Java 方式:通过使用 equals() 方法。当需要额外的灵活性,可以使用参数匹配器。
@Test
public void testAnyInt() {
List list = mock(ArrayList.class);
when(list.get(anyInt())).thenReturn("element");
System.out.println(list.get(0));
verify(list).get(anyInt());
}
自定义参数:可以实现 org.mockito.ArgumentMatcher 接口,请查看 Javadoc 中 ArgumentMatcher 类。
class ListOfTwoElements implements ArgumentMatcher<List> {
public boolean matches(List list) {
return list.size() == 2;
}
public String toString() {
//printed in verification errors
return "[list of 2 elements]";
}
}
@Test
public void testArgs() {
List mock = mock(List.class);
//自定义参数(需要size=2的list)
when(mock.addAll(argThat(new ListOfTwoElements()))).thenReturn(true);
//构造size=2的list
mock.addAll(Arrays.asList("one", "two"));
//验证
verify(mock).addAll(argThat(new ListOfTwoElements()));
}
调用额外的调用数字times()/atLeast()/never()/atMost()
@Test
public void testCount() {
List mockedList = mock(List.class);
mockedList.add("once");
mockedList.add("twice");
mockedList.add("twice");
mockedList.add("three times");
mockedList.add("three times");
mockedList.add("three times");
//下面两个verify()是一样的,times()默认是1次
verify(mockedList).add("once");
verify(mockedList, times(1)).add("once");
//调用验证的确切数量
verify(mockedList, times(2)).add("twice");
verify(mockedList, times(3)).add("three times");
//验证使用 never() = times(0)
verify(mockedList, never()).add("never happened");
//验证至少、最多:atLeast()/atMost()
verify(mockedList, atLeastOnce()).add("three times");
verify(mockedList, never()).add("five times");
verify(mockedList, atMost(5)).add("three times");
}
处理异常Mock
@Test(expected = RuntimeException.class)
public void testThrowException() {
List mockedList = mock(List.class);
doThrow(new RuntimeException()).when(mockedList).clear();
mockedList.clear();
}
有序的验证
@Test
public void testOrder() {
// A.必须按特定顺序调用其方法的单个模拟
List singleMock = mock(List.class);
//单个模拟
singleMock.add("was added first");
singleMock.add("was added second");
//为单个模拟创建一个 inOrder 验证器
InOrder inOrder = inOrder(singleMock);
//确保首先使用 "首先 was added first 添加,然后was added second 添加" 来调用 add
inOrder.verify(singleMock).add("was added first");
inOrder.verify(singleMock).add("was added second");
// B. 必须以特定顺序使用的多个模拟
List firstMock = mock(List.class);
List secondMock = mock(List.class);
//使用模拟
firstMock.add("was called first");
secondMock.add("was called second");
//创建 inOrder 对象,传递需要按顺序验证的Mock
//有序验证是为了灵活,因此你不必一个接一个验证所有的交互
InOrder inOrder2 = inOrder(firstMock, secondMock);
//以下验证 firstMock 在 secondMock 之前被调用
inOrder2.verify(firstMock).add("was called first");
inOrder2.verify(secondMock).add("was called second");
}
寻找多余的调用
注意:不建议 verifyNoMoreInteractions() 在每个测试方法中使用。 verifyNoMoreInteractions() 是从交互测试工具包一个方便的断言。只有与它的相关时才使用它,滥用它导致难以维护。
@Test(expected = NoInteractionsWanted.class)
public void testVerifyNoMoreInteractions() {
List mockedList = mock(List.class);
//mocks
mockedList.add("one");
mockedList.add("two");
verify(mockedList).add("one");
//verify(mockedList).add("two");
//下面这个验证将失败,抛出 NoInteractionsWanted
verifyNoMoreInteractions(mockedList);
}
Stubbing 连续调用(迭代器式的 stubbing)
@Test(expected = RuntimeException.class)
public void testCircleCall() {
List mock = mock(List.class);
when(mock.get(anyInt()))
.thenReturn("bar")
.thenReturn("foo")
.thenThrow(new RuntimeException());
//第一次调用:打印 bar
System.out.println(mock.get(0));
//第二次调用: 打印 "foo"
System.out.println(mock.get(1));
//第三次调用:抛出 RuntimeException
System.out.println(mock.get(2));
}
/**
* 优化写法
*/
@Test(expected = RuntimeException.class)
public void testCircleCall2() {
List mock = mock(List.class);
when(mock.get(anyInt()))
.thenReturn("bar", "foo").thenThrow(new RuntimeException());
//第一次调用:打印 bar
System.out.println(mock.get(0));
//第二次调用: 打印 "foo"
System.out.println(mock.get(1));
//第三次调用:抛出 RuntimeException
System.out.println(mock.get(2));
}
回调 Stubbing
使用泛型 Answer 接口,然而,这是不包括在最初的 Mockito 另一个有争议的功能,建议您只需用 thenReturn() 或 thenThrow() 来 stubbing ,这在测试/测试驱动中应用简洁与简单的代码足够了,除非你有一个需要 stub 到泛型 Answer 接口,举个栗子。
@Test
public void testAnswer() {
List mock = mock(ArrayList.class);
when(mock.get(anyInt())).thenAnswer(new Answer() {
public Object answer(InvocationOnMock invocation) {
Object[] args = invocation.getArguments();
Object mock = invocation.getMock();
return "调用方法的参数: " + Arrays.toString(args);
}
});
//打印 "调用方法的参数: [0]"
System.out.println(mock.get(anyInt()));
}
doXxx() 家族方法
在调用 when() 的相应地方可以使用 doThrow(),doAnswer(), doNothing(), doReturn() 和doCallRealMethod(),主要原因是提高可读性和与 doAnswer() 保持一致性。
@Test
public void testDoXxx() {
List mock1 = mock(ArrayList.class);
when(mock1.get(anyInt())).thenReturn("item");
System.out.println(mock1.get(anyInt()));
List mock2 = mock(ArrayList.class);
//可读性更好
doReturn("item do return.").when(mock2).get(anyInt());
System.out.println(mock2.get(anyInt()));
}
mock 静态方法
如果要 Mock 静态方法,首先在类的开头增加注解:@PrepareForTest({ClassNameA.class})。 在需要 Mock 类方法的之前,增加代码:PowerMockito.mockStatic(ClassNameA.class) 即可。
mock 空方法
mock一个空方法,非常简单,就是调用doNothing()... when()...
参考
在下一篇文章中,我们将继续探讨更多知识,敬请期待!
感谢您的阅读,如果您觉得这篇文章对你有帮助,欢迎点赞和分享!