单元测试神器: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注入。

相关推荐
FreeBuf_2 小时前
黄金旋律IAB组织利用暴露的ASP.NET机器密钥实施未授权访问
网络·后端·asp.net
张小洛3 小时前
Spring AOP 是如何生效的(入口源码级解析)?
java·后端·spring
why技术4 小时前
也是出息了,业务代码里面也用上算法了。
java·后端·算法
白仑色6 小时前
完整 Spring Boot + Vue 登录系统
vue.js·spring boot·后端
ZhangApple8 小时前
微信自动化工具:让自己的微信变成智能机器人!
前端·后端
Codebee8 小时前
OneCode 3.0: 注解驱动的Spring生态增强方案
后端·设计模式·架构
bobz9658 小时前
kubevirt virtinformers
后端
LuckyLay8 小时前
Django专家成长路线知识点——AI教你学Django
后端·python·django
Java微观世界8 小时前
征服Java三大特性:封装×继承×多态+this/super高阶指南
后端
Java技术小馆8 小时前
RPC vs RESTful架构选择背后的技术博弈
后端·面试·架构