当前位置: 首页 > news >正文

如何用 Mockito 玩转单元测试

介绍

Mockito 是一个广泛使用的 Java 测试框架,它提供了简洁而强大的功能,用于模拟(mock)和验证对象的行为,尤其是在单元测试中。

当我们需要测试某个类的功能时,但又不希望依赖其外部组件或复杂的对象时,可以使用 Mockito 来创建模拟对象,这些模拟对象可以控制方法返回值、抛出异常或执行特定的逻辑。Mockito 使得测试变得更加独立、可靠和可维护,特别是在测试依赖较多或外部系统交互的代码时。


从一个例子开始

以下示例模拟了 List,因为大多数人都熟悉 List 的使用方法,例如 add、get、clear 方法。

import static org.mockito.Mockito.*;// 开始 mock List对象
List mockedList = mock(List.class);// 使用 mock 对象
mockedList.add("one");
mockedList.clear();// 验证
verify(mockedList).add("one");
verify(mockedList).clear();

创建模拟对象后,mock 将记住所有交互(交互一般是函数调用)。然后,你可以有选择地验证感兴趣的任何交互。


添加存根 stubbing

存根是 stubbing 的中文翻译,由于存根一词并不利于理解,因此下文索性统一使用 stubbing

stubbing 是模拟(mock)对象行为的过程,指的是为模拟对象的方法调用提供预定义的返回值或行为。简单来说,stubbing 就是告诉 Mockito,当模拟对象的方法被调用时,应该返回什么结果或者执行什么操作。

LinkedList mockedList = mock(LinkedList.class);// stubbing
when(mockedList.get(0)).thenReturn("first");
when(mockedList.get(1)).thenThrow(new RuntimeException());// 打印 first
System.out.println(mockedList.get(0));// 抛出 RuntimeException
System.out.println(mockedList.get(1));// 打印 null
System.out.println(mockedList.get(999));// verify stubbing 一般是多余的,这里为了演示
verify(mockedList).get(0);

默认情况下,Mockito 会为所有方法的返回值提供自动模拟。例如,返回 null、原始值(如 0 表示 int 或 Integer,false 表示 boolean 或 Boolean)或空集合(如空 List、空 Map 等)。这种默认行为使得即使没有明确进行 Stubbing,模拟对象仍然能安全地返回合理的默认值。

Stubbing 是指通过明确的指令为模拟对象的方法调用提供预定义的返回值或行为。一旦对某个方法进行了 Stubbing,不管该方法被调用多少次,都会返回 stubbing 值。 

在多次对同一个方法进行 Stubbing 后,最后一次设置的 Stubbing 会覆盖之前的所有设置


参数匹配器 anyXXX()

any() 系列:匹配任意值(包括null),比如:

  • anyString()
  • anyInt()
  • anyList()
  • any(Class<T>)

eq():匹配特定值(需精确相等)。

// 对于 anyInt() 都将返回 element
when(mockedList.get(anyInt())).thenReturn("element");// 打印 element
System.out.println(mockedList.get(999));// 可以用 anyInt() 做验证
verify(mockedList).get(anyInt());

如果使用参数匹配器,则所有参数必须由参数匹配器提供。

// 验证调用了此方法,并且第三个参数为 third argument
verify(mock).someMethod(anyInt(), anyString(), eq("third argument"));// 抛异常,因为第三个参数是具体值
verify(mock).someMethod(anyInt(), anyString(), "third argument");

不能在 verify 或 stubbing 方法之外使用 anyXXX(), eq() 等方法。


验证调用次数 verify

// 开始 mock
mockedList.add("once");mockedList.add("twice");
mockedList.add("twice");mockedList.add("three times");
mockedList.add("three times");
mockedList.add("three times");// 以下两种方式完全等价,验证只调用过一次
// 强调一下,第一种方式仅代表调用过一次,不是至少一次
verify(mockedList).add("once");
verify(mockedList, times(1)).add("once");// 校验调用的确切次数
verify(mockedList, times(2)).add("twice");
verify(mockedList, times(3)).add("three times");// 校验没有调用过
verify(mockedList, never()).add("never happened");
verify(mockedList, times(0)).add("never happened");// 校验至少、至多调用次数
verify(mockedList, atMostOnce()).add("once");
verify(mockedList, atLeastOnce()).add("three times");
verify(mockedList, atLeast(2)).add("three times");
verify(mockedList, atMost(5)).add("three times");

注意:times(1) 是默认值。 因此,显式使用 times(1) 可以是省略


排除冗余调用 verifyNoMoreInteractions

verifyNoMoreInteractions 用于验证模拟对象上是否没有多余的交互发生。

该方法通常在测试的最后阶段使用,确保模拟对象的行为完全符合预期。例如,在一个测试中,可能先调用某些方法并验证这些调用,之后使用 verifyNoMoreInteractions 确保没有其他未预料到的调用发生。

// 开始 mock
mockedList.add("one");
mockedList.add("two");// 调用过 add("one")
verify(mockedList).add("one");// 失败:因为还调用过 add("two")
verifyNoMoreInteractions(mockedList);


对连续调用进行 stubbing

有时我们需要为相同的 stubbing 使用不同的 返回值/异常,典型的用例可能是模拟迭代器。

when(mock.someMethod("some arg"))
.thenThrow(new RuntimeException())
.thenReturn("foo");// 第一次调用 throws runtime exception
mock.someMethod("some arg");// 第二次调用 打印foo
System.out.println(mock.someMethod("some arg"));// 之后的调用都将打印foo,因为以最后一次为准
System.out.println(mock.someMethod("some arg"));

连续 stubbing 的可以简化代码为:

// 直接模拟调用一次、两次、三次的返回结果
when(mock.someMethod("some arg")).thenReturn("one", "two", "three");

警告:如果不是流式调用,而是使用相同的匹配器或参数进行多个 stubbing ,则会以最后一个 stubbing 为准(即覆盖前面所有的 stubbing)

when(mock.someMethod("some arg")).thenReturn("one");
when(mock.someMethod("some arg")).thenReturn("two");// 接下来,所有的 mock.someMethod("some arg") 都将返回 two


模拟回调 Answer

这是另一个有争议的功能,最初并未包含在 Mockito 中。因为使用 thenReturn() 或 thenThrow()  函数应该足以进行测试任何干净简单的代码。但是,如果确实需要使用通用 Answer 接口进行 stubbing,下面是一个示例:

when(mock.someMethod(anyString())).thenAnswer(new Answer() {public Object answer(InvocationOnMock invocation) {Object[] args = invocation.getArguments();Object mock = invocation.getMock();return "called with arguments: " + Arrays.toString(args);}
});// 下面将打印 called with arguments: [foo]
System.out.println(mock.someMethod("foo"));


doXXX() 系列方法

方法作用使用场景示例
doReturn()指定方法返回值用于无法在 when() 中调用的方法(如 finalprivatedoReturn("value").when(mock).method()
doThrow()模拟抛出异常用于模拟抛出异常的场景,常见于 void 方法或带异常的调用doThrow(new Exception()).when(mock).method()
doAnswer()自定义行为用于根据输入自定义方法的返回值或行为doAnswer(invocation -> {}).when(mock).method()
doNothing()模拟不做任何事情用于 void 方法,确保它被调用但不执行实际逻辑doNothing().when(mock).method()
doCallRealMethod()调用真实方法用于测试中需要部分调用真实方法的场景

doCallRealMethod().when(mock).method()


监视真实物体 spy

使用 spy 时,调用的是实际方法,所以需要小心。Spy 适用于遗留代码或特殊场景,但不应过度使用。Mockito 1.8 之前,spy 并不支持真正的部分模拟,认为部分模拟是一个不好的做法。但后来发现,在某些情况下(比如与第三方接口交互或临时重构遗留代码),部分模拟是有用的。

List list = new LinkedList();
List spy = spy(list);// stubbing size方法
when(spy.size()).thenReturn(100);// 调用真实方法,添加两个元素
spy.add("one");
spy.add("two");// 调用真实方法,打印one
System.out.println(spy.get(0));// 调用 stubbing 方法吗打印 100
System.out.println(spy.size());


重置模拟对象 reset

List mock = mock(List.class);
when(mock.size()).thenReturn(10);
mock.add(1);// reset 之后以上的所有 stubbing 将会失效
reset(mock);


严格 Stubbing

@Test
public void givenUnusedStub() {// 假设这个 stubbing 没有使用when(mockList.add("one")).thenReturn(true);// 这个使用了when(mockList.get(anyInt())).thenReturn("hello");assertEquals("List should contain hello", "hello", mockList.get(1));
}

当我们运行这个单元测试时,Mockito 将检测未使用的 stubbing 并抛出 UnnecessaryStubbingException:表明第一个 when 是多余的,因为没有在单元测试中调用此方法

Mockito 也提供了一些方法绕过严格 stubbing 的检查:

  • Mockito.lenient()
  • @MockitoSettings(strictness = Strictness.LENIENT)


模拟静态方法 mockStatic

 为了确保静态模拟保持临时状态,建议在 try-with-resources 构造中定义范围。 在下面的示例中,除非被模拟,否则 Foo 类型的 static 方法将返回 foo:

assertEquals("foo", Foo.method());try (MockedStatic mocked = mockStatic(Foo.class)) {mocked.when(Foo::method).thenReturn("bar");assertEquals("bar", Foo.method());mocked.verify(Foo::method);
}
// 还是成功,不会被 try 的内部影响
assertEquals("foo", Foo.method());


模拟对象构造 mockConstruction

为了确保构造函数 mock 保持临时状态,建议在 try-with-resources 构造中定义范围。 在下面的示例中,Foo 类型的构造将生成一个 mock:

assertEquals("foo", new Foo().method());try (MockedConstruction mocked = mockConstruction(Foo.class)) {Foo foo = new Foo();when(foo.method()).thenReturn("bar");assertEquals("bar", foo.method());verify(foo).method();
}assertEquals("foo", new Foo().method());


捕获参数 ArgumentCaptor

ArgumentCaptor 用于捕获方法调用时传递的参数,以便后续进行验证或断言。

ArgumentCaptor 必须与 verify 一起使用,单独使用 captor.capture() 不会产生任何效果。捕获的参数类型必须与实际的参数类型匹配,否则会抛出异常。

List mockList = Mockito.mock(List.class);
ArgumentCaptor<String> arg = ArgumentCaptor.forClass(String.class); mockList.add("one");
Mockito.verify(mockList).add(arg.capture());ssertEquals("one", arg.getValue());


参考

Mockito - mockito-core 5.18.0 javadoc

https://www.baeldung.com/mockito-series

http://www.dtcms.com/a/268813.html

相关文章:

  • 闲庭信步使用图像验证平台加速FPGA的开发:第三课——YCbCr转RGB的FPGA实现
  • 搜广推校招面经八十八
  • Linux批量执行工具脚本使用指南:一键运行多个release-dev.sh脚本
  • macOS运行python程序遇libiomp5.dylib库冲突错误解决方案
  • 【STM32】const 变量存储学习笔记
  • 【论文阅读】CogView: Mastering Text-to-Image Generation via Transformers
  • 文心一言4.5开源模型测评:ERNIE-4.5-0.3B超轻量模型部署指南
  • React19 新增Hooks:useOptimistic
  • 巧借东风:32位栈迁移破解ciscn_2019_es_2的空间困局
  • maven 发布到中央仓库-01-概览
  • 23、企业租赁管理(Rent)全流程指南:从资产盘活到价值最大化的数字化实践
  • Dify工作流实战:输入接口名,自动生成带源码的Markdown API文档(附完整Prompt)
  • Linux 文件系统与日志分析(补充)
  • 报错 400 和405解决方案
  • 海外短剧系统开发:PC端与H5端的全栈实践与深度解析
  • Day07- 管理并发和并行挑战:竞争条件和死锁
  • 在bash shell 函数传递数组的问题2
  • 【DeepSeek实战】17、MCP地图服务集成全景指南:高德、百度、腾讯三大平台接入实战
  • PCIE Ack/Nak机制详解
  • Unity 实现与 Ollama API 交互的实时流式响应处理
  • ES 压缩包安装
  • socket接口api的深度探究
  • 初识Neo4j之Cypher
  • 【Unity笔记】Unity 粒子系统 Triggers 使用解析:监听粒子进入与离开区域并触发事件
  • 在 macOS 上安装和测试 LibreOffice
  • 深入解析TCP:可靠传输的核心机制与实现逻辑(三次握手、四次挥手、流量控制、滑动窗口、拥塞控制、慢启动、延时应答、面向字节流、粘包问题)
  • 借助HarmonyOS SDK,《NBA巅峰对决》实现“分钟级启动”到“秒级进场”
  • 【7】PostgreSQL 事务
  • SRAM与三级缓存(L1/L2/L3 Cache)的关系
  • 芯谷科技--高性能双运算放大器D358