单元测试利器-Mockito

356 阅读7分钟

如果您有任何问题或想要讨论的话题,欢迎在评论区留言,我将尽快回复。

欢迎扫描下方二维码,阅读更多成体系的干货文章!

image.png

上一篇《单元测试》初步介绍了 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));
}

小结:

  1. 默认情况下,所有方法都会返回值,一个 mock 要么返回默认值要么返回 null,例如,一个原始/基本类型的包装值或适当的空集, int/Integer 就是 0, boolean/Boolean 就是 false。
  2. 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()...

参考

Mockito 官方文档

在下一篇文章中,我们将继续探讨更多知识,敬请期待!

感谢您的阅读,如果您觉得这篇文章对你有帮助,欢迎点赞和分享!