单元测试神器:Mockito

阅读说明:

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

简介

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

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

1. 环境准备

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

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

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

xml 复制代码
<dependency>
  <groupId>org.mockito</groupId>
  <artifactId>mockito-junit-jupiter</artifactId>
  <version>5.3.1</version>
  <scope>test</scope>
</dependency>

2. 基本 Mock 用法

创建 Mock 对象

arduino 复制代码
import static org.mockito.Mockito.*;

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

配置 Mock 行为 (when-thenReturn)

less 复制代码
// 配置 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)

scss 复制代码
// 验证 get(0) 被调用了一次
verify(mockedList).get(0);

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

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

3. Spy 用法

Spy 是对真实对象的包装,默认会调用真实方法,但你可以选择性地 stub 某些方法。

scss 复制代码
List<String> realList = new ArrayList<>();
List<String> spiedList = spy(realList);

// 可以调用真实方法
spiedList.add("one");
spiedList.add("two");

// 验证真实行为
assertEquals(2, spiedList.size());

// 可以 stub 特定方法
when(spiedList.size()).thenReturn(100);

// 现在 size() 方法被 stubbed 了
assertEquals(100, spiedList.size());

// 其他方法仍然调用真实实现
assertEquals("one", spiedList.get(0));

4. 高级 Stubbing

thenAnswer 用法

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

vbnet 复制代码
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"));

抛出异常

less 复制代码
when(mockedList.get(anyInt())).thenThrow(new RuntimeException("Boom!"));

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

连续 Stubbing

less 复制代码
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 提供了多种参数匹配器,使验证更加灵活:

less 复制代码
// 使用 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)

验证调用次数

scss 复制代码
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");

验证调用顺序

ini 复制代码
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 是一个强大的工具,它允许你在验证方法调用时捕获并检查传递的参数。这在以下场景特别有用:

  • 需要验证传递给方法的复杂对象
  • 需要检查方法调用时参数的内部状态
  • 需要断言多次调用中的不同参数值
scss 复制代码
@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不仅能够捕获基本类型,还支持自定义对象。

验证无更多交互

scss 复制代码
mockedList.add("one");
mockedList.clear();

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

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

7. 完整示例

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

less 复制代码
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注入。

相关推荐
程序员爱钓鱼22 分钟前
Python 编程实战:环境管理与依赖管理(venv / Poetry)
后端·python·trae
w***488223 分钟前
Spring Boot3.x集成Flowable7.x(一)Spring Boot集成与设计、部署、发起、完成简单流程
java·spring boot·后端
程序员爱钓鱼24 分钟前
Python 编程实战 :打包与发布(PyInstaller / pip 包发布)
后端·python·trae
IT_陈寒1 小时前
Redis 性能提升30%的7个关键优化策略,90%开发者都忽略了第3点!
前端·人工智能·后端
Victor3561 小时前
Redis(137)Redis的模块机制是什么?
后端
Victor3561 小时前
Redis(136)Redis的客户端缓存是如何实现的?
后端
不知更鸟6 小时前
Django 项目设置流程
后端·python·django
黄昏恋慕黎明8 小时前
spring MVC了解
java·后端·spring·mvc
G探险者10 小时前
为什么 VARCHAR(1000) 存不了 1000 个汉字? —— 详解主流数据库“字段长度”的底层差异
数据库·后端·mysql
百锦再10 小时前
第18章 高级特征
android·java·开发语言·后端·python·rust·django