单元测试神器Mockito,一网打尽用法

571 阅读6分钟

阅读说明:

本文为原创文章,转发请注明出处。如果觉得文章不错,请点赞、收藏、关注一下,您的认可是我写作的动力。

简介

写单元测试时,你是否也受够了数据库连接、第三方接口这些外部依赖的困扰?明明只想验证核心业务逻辑,却不得不花大量时间处理各种无关的依赖——就像想喝杯咖啡,却得先种咖啡豆一样麻烦。

这就要请出Mockito了。Mockito 是 Java 中最流行的 mocking 框架之一,它可以帮助你创建和配置 mock 对象,从而更轻松地编写单元测试。本教程将介绍 Mockito 的核心功能,包括 mock、spy、when、return、answer、matcher 和 verify 的用法。

1. 环境准备

首先,确保你的项目中已经添加了 Mockito 依赖。对于 Maven 项目:

<dependency>
  <groupId>org.mockito</groupId>
  <artifactId>mockito-core</artifactId>
  <version>5.3.1</version> <!-- 使用最新版本 -->
  <scope>test</scope>
</dependency>

对于 JUnit 5 用户,还可以添加 Mockito 扩展:

<dependency>
  <groupId>org.mockito</groupId>
  <artifactId>mockito-junit-jupiter</artifactId>
  <version>5.3.1</version>
  <scope>test</scope>
</dependency>

2. 基本 Mock 用法

创建 Mock 对象

import static org.mockito.Mockito.*;

// 创建一个 List 的 mock 对象
List<String> mockedList = mock(List.class);

配置 Mock 行为 (when-thenReturn)

设置动作发生时,预期返回的值。格式:

when(mockObjest.someMethod()).thenReturn(value)
// 配置 mock 对象的行为
when(mockedList.get(0)).thenReturn("first");

这句话 Mockito 会解析为:当对象 mockedList 调用 get()方法,并且参数为 0 时,返回结果为"first",这相当于定制了我们 mock 对象的行为结果(mock LinkedList 对象为 mockedList,指定其行为 get(0),则返回结果为 "first")。

// 配置 mock 对象的行为
when(mockedList.get(0)).thenReturn("first");
when(mockedList.get(1)).thenReturn("second");

// 使用 mock 对象
System.out.println(mockedList.get(0)); // 输出 "first"
System.out.println(mockedList.get(1)); // 输出 "second"
System.out.println(mockedList.get(2)); // 输出 null,因为我们没有为索引 2 配置行为

验证交互 (verify)

// 验证 get(0) 被调用了一次
verify(mockedList).get(0);

// 验证 get(1) 被调用了至少一次
verify(mockedList, atLeastOnce()).get(1);

// 验证 get(2) 从未被调用
verify(mockedList, never()).get(2);

3. Spy 用法

Spy 是对真实对象的包装,默认会调用真实方法,但你可以选择性地 stub 某些方法。Spy可以用when-thenReturn,会先执行真实方法再返回mock值。也可以doReturn-when, 直接返回mock值,doReturn-when格式如下:

doReturn(value).when(mockObjest).someMethod();

两者区别如下:

when().thenReturn() :

    • 会先调用真实方法,然后再用模拟值覆盖返回
    • 如果真实方法有副作用或不可调用(如抛出异常),测试会失败
    • 语法更直观,适合大多数情况, 适用于mock,

doReturn().when() :

    • 完全跳过真实方法的调用,直接返回模拟值
    • 适用于需要避免调用真实方法的情况
    • 语法稍显不直观,但在某些场景下是必要的

使用场景总结:

场景推荐方法原因
普通mock对象when().thenReturn()简单直接, void 方法必须使用 doNothing-when 语法
普通spy对象doReturn().when()避免调用真实方法,防止真实方法抛异常或者其他影响。
List<String> realList = new ArrayList<>();
List<String> spiedList = spy(realList);

when(spiedList.add("1").thenReturn(true);
doReturn(false).when(spiedList).add("2");
boolean add1 = spiedList.add("1");  //add1:true
boolean add2 = spiedList.add("2");  //add2: false

// 验证真实行为
assertEquals(1, spiedList.size());   //里面只有"1"

4. 高级 Stubbing

Answer 用法(thenAnswer或者doAnswer)

当需要根据输入动态决定返回值时,可以使用 thenAnswer或者doAnswer:

Map<String, String> mockMap = mock(Map.class);

when(mockMap.get(anyString())).thenAnswer(invocation -> {
    String key = invocation.getArgument(0);
    return "value_for_" + key;
});

assertEquals("value_for_foo", mockMap.get("foo"));
assertEquals("value_for_bar", mockMap.get("bar"));

抛出异常

when(mockedList.get(anyInt())).thenThrow(new RuntimeException("Boom!"));

assertThrows(RuntimeException.class, () -> mockedList.get(0));

连续 Stubbing

when(mockedList.size())
.thenReturn(1)
.thenReturn(2)
.thenThrow(new RuntimeException("Too many calls"));

assertEquals(1, mockedList.size());
assertEquals(2, mockedList.size());
assertThrows(RuntimeException.class, () -> mockedList.size());

5. 参数匹配器 (Argument Matchers)

Mockito 提供了多种参数匹配器,使验证更加灵活:

// 使用 anyXxx 系列匹配器
when(mockedList.get(anyInt())).thenReturn("element");

// 使用 eq 匹配特定值
when(mockedList.contains(eq("specific"))).thenReturn(true);

// 使用自定义匹配器
when(mockedList.add(argThat(s -> s.length() > 5))).thenReturn(true);

// 验证
mockedList.get(999); // 任何整数都可以
mockedList.contains("specific");
mockedList.add("long string");

verify(mockedList).get(anyInt());
verify(mockedList).contains(eq("specific"));
verify(mockedList).add(argThat(s -> s.length() > 5));

常用匹配器包括:

  • any(), any(Class<T>)
  • anyInt(), anyString(), anyList(), 等等
  • eq(value) - 匹配特定值
  • isNull(), isNotNull()
  • argThat(matcher) - 自定义匹配

6. 验证详细用法(verify)

验证调用次数

mockedList.add("once");

mockedList.add("twice");
mockedList.add("twice");

mockedList.add("three times");
mockedList.add("three times");
mockedList.add("three times");

// 验证默认调用一次 (times(1) 是默认的)
verify(mockedList).add("once");

// 验证确切调用次数
verify(mockedList, times(2)).add("twice");
verify(mockedList, times(3)).add("three times");

// 验证从未调用
verify(mockedList, never()).add("never happened");

// 验证至少/至多调用次数
verify(mockedList, atLeastOnce()).add("three times");
verify(mockedList, atLeast(2)).add("three times");
verify(mockedList, atMost(5)).add("three times");

验证调用顺序

List firstMock = mock(List.class);
List secondMock = mock(List.class);

firstMock.add("first");
secondMock.add("second");

// 创建 InOrder 对象并验证调用顺序
InOrder inOrder = inOrder(firstMock, secondMock);
inOrder.verify(firstMock).add("first");
inOrder.verify(secondMock).add("second");

验证调用参数

Mockito不仅能够验证调用次数,还能够对参数进行验证。 ArgumentCaptor 是一个强大的工具,它允许你在验证方法调用时捕获并检查传递的参数。这在以下场景特别有用:

  • 需要验证传递给方法的复杂对象
  • 需要检查方法调用时参数的内部状态
  • 需要断言多次调用中的不同参数值
@Test
void testMultipleArguments() {
    Map<String, Integer> mockedMap = mock(Map.class);
    mockedMap.put("key1", 100);
    mockedMap.put("key2", 200);
    
    ArgumentCaptor<String> keyCaptor = ArgumentCaptor.forClass(String.class);
    ArgumentCaptor<Integer> valueCaptor = ArgumentCaptor.forClass(Integer.class);
    
    verify(mockedMap, times(2)).put(keyCaptor.capture(), valueCaptor.capture());
    
    List<String> keys = keyCaptor.getAllValues();
    List<Integer> values = valueCaptor.getAllValues();
    
    assertEquals("key1", keys.get(0));
    assertEquals(100, (int) values.get(0));
    assertEquals("key2", keys.get(1));
    assertEquals(200, (int) values.get(1));
}

ArgumentCaptor不仅能够捕获基本类型,还支持自定义对象。

验证无更多交互

mockedList.add("one");
mockedList.clear();

// 验证所有交互
verify(mockedList).add("one");
verify(mockedList).clear();

// 验证没有其他交互
verifyNoMoreInteractions(mockedList);

7. 完整示例

下面是一个完整的测试类示例,展示了 Mockito 的各种用法:

import org.junit.jupiter.api.Test;
import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;

class MockitoCompleteExampleTest {

    @Test
    void testMockitoFeatures() {
        // 1. 创建 mock 对象
        List<String> mockedList = mock(List.class);

        // 2. 使用 when-thenReturn 配置行为
        when(mockedList.get(0)).thenReturn("first");
        when(mockedList.get(1)).thenReturn("second");
        when(mockedList.size()).thenReturn(10);

        // 3. 使用 mock 对象
        assertEquals("first", mockedList.get(0));
        assertEquals("second", mockedList.get(1));
        assertEquals(10, mockedList.size());

        // 4. 使用 thenAnswer 动态生成返回值
        when(mockedList.get(anyInt())).thenAnswer(invocation -> {
            int index = invocation.getArgument(0);
            return "element_" + index;
        });
        assertEquals("element_5", mockedList.get(5));

        // 5. 使用 spy 包装真实对象
        List<String> realList = new ArrayList<>();
        List<String> spiedList = spy(realList);

        spiedList.add("real");
        assertEquals(1, spiedList.size());
        assertEquals("real", spiedList.get(0));

        // 6. stub spy 的部分方法
        when(spiedList.size()).thenReturn(100);
        assertEquals(100, spiedList.size());

        // 7. 验证交互
        verify(mockedList).get(0);
        verify(mockedList, times(2)).get(anyInt());
        verify(spiedList).add("real");

        // 8. 使用参数匹配器
        when(mockedList.contains(anyString())).thenReturn(true);
        assertTrue(mockedList.contains("anything"));

        // 9. 验证调用顺序
        mockedList.add("first");
        mockedList.add("second");

        InOrder inOrder = inOrder(mockedList);
        inOrder.verify(mockedList).add("first");
        inOrder.verify(mockedList).add("second");
    }
}

8. 总结

Mockito 是一个功能强大且灵活的 mocking 框架,本教程涵盖了它的核心功能:

  • mock() - 创建 mock 对象
  • spy() - 创建 spy 对象(部分 mock)
  • when().thenReturn() - 配置 mock 行为
  • thenAnswer() - 动态生成返回值
  • 参数匹配器 (any(), eq(), argThat() 等) - 灵活匹配参数
  • verify() - 验证交互行为

Mockito有效协助我们编写单元测试用例,提高代码健壮性,让测试变得简单、专注而高效。Mockito另外一大优点是可以跟Spring 测试框架无缝集成,有专门的注解支持spring注入。