1. 简介
在大多数情况下,Mockito 提供的默认 mock 配置已经足够使用。
但有时我们可能需要在创建 mock 对象时提供额外的配置选项。这在调试、处理遗留代码或覆盖某些边界场景时尤其有用。
之前我们已经了解了如何使用 lenient mocks(宽松 mock)。在这篇文章中,我们将继续学习 MockSettings 接口提供的其他实用功能。
2. Mock 配置详解
简单来说,MockSettings 接口提供了一套 Fluent API,让我们可以在创建 mock 对象时轻松添加和组合各种配置。
当我们创建一个 mock 对象时,它其实已经携带了一组默认配置。例如:
List mockedList = mock(List.class);
在内部,Mockito 的 mock
方法会委托给另一个重载方法,并传入一组默认配置:
public static <T> T mock(Class<T> classToMock) {
return mock(classToMock, withSettings());
}
我们来看看这些默认配置:
public static MockSettings withSettings() {
return new MockSettingsImpl().defaultAnswer(RETURNS_DEFAULTS);
}
可以看到,Mockito 默认只配置了 mock 对象的默认返回行为。通常,RETURNS_DEFAULTS 会返回一些“空值”,比如 null
、0、空集合等。
✅ 关键点在于:如果需要,我们可以自定义这些配置项。
接下来的章节中,我们会通过一些实际例子来说明这些配置的实际用途。
3. 自定义默认返回值
了解了 mock 配置的基本原理后,我们来看看如何自定义 mock 对象的默认返回值。
假设我们有如下代码:
PizzaService service = mock(PizzaService.class);
Pizza pizza = service.orderHouseSpecial();
PizzaSize size = pizza.getSize();
运行这段代码时,不出意外会抛出 NullPointerException,因为 orderHouseSpecial()
方法没有被 stub,返回了 null
。
这在大多数情况下没问题,但在处理遗留代码时,如果 mock 对象层级较深,排查这类异常可能会很麻烦。
为了解决这个问题,我们可以通过 mock 配置来指定一个不同的默认返回行为:
PizzaService pizzaService = mock(PizzaService.class, withSettings().defaultAnswer(RETURNS_SMART_NULLS));
使用 RETURNS_SMART_NULLS 时,Mockito 会提供更有意义的错误提示,明确指出是哪个方法调用导致了问题:
org.mockito.exceptions.verification.SmartNullPointerException:
You have a NullPointerException here:
-> at com.baeldung.mockito.mocksettings.MockSettingsUnitTest.whenServiceMockedWithSmartNulls_thenExceptionHasExtraInfo(MockSettingsUnitTest.java:45)
because this method call was *not* stubbed correctly:
-> at com.baeldung.mockito.mocksettings.MockSettingsUnitTest.whenServiceMockedWithSmartNulls_thenExceptionHasExtraInfo(MockSettingsUnitTest.java:44)
pizzaService.orderHouseSpecial();
这在调试测试代码时能节省不少时间。
Answers 枚举还提供了其他几个常用的默认行为:
- RETURNS_DEEP_STUBS:返回“深层 stub”,✅ 特别适合用于 Fluent API
- RETURNS_MOCKS:返回普通默认值(如空集合、空字符串),之后尝试返回 mock 对象
- CALLS_REAL_METHODS:未 stub 的方法会调用真实实现
4. 命名 Mock 与详细日志
我们可以通过 MockSettings 的 name()
方法给 mock 对象起个名字,这对调试非常有帮助,因为该名称会出现在所有验证错误信息中:
PizzaService service = mock(PizzaService.class, withSettings()
.name("pizzaServiceMock")
.verboseLogging()
.defaultAnswer(RETURNS_SMART_NULLS));
在这个例子中,我们还通过 verboseLogging()
启用了详细日志功能。
⚠️ 启用后,所有在该 mock 上的方法调用都会实时输出到标准输出流中,非常适合在调试测试时排查 mock 交互问题。
运行测试时,控制台会输出类似如下信息:
pizzaServiceMock.orderHouseSpecial();
invoked: -> at com.baeldung.mockito.mocksettings.MockSettingsUnitTest.whenServiceMockedWithNameAndVerboseLogging_thenLogsMethodInvocations(MockSettingsUnitTest.java:36)
has returned: "Mock for Pizza, hashCode: 366803687" (com.baeldung.mockito.fluentapi.Pizza$MockitoMock$168951489)
💡 注意:如果使用 @Mock
注解创建 mock,mock 的名称会自动使用字段名。
5. 实现额外接口
有时候我们可能需要让 mock 对象实现额外的接口。这在处理无法重构的遗留代码时特别有用。
假设我们有这样一个接口:
public interface SpecialInterface {
// Public methods
}
以及一个类:
public class SimpleService {
public SimpleService(SpecialInterface special) {
Runnable runnable = (Runnable) special;
runnable.run();
}
// More service methods
}
虽然这段代码不符合 clean code 原则,但如果我们必须为其编写单元测试,就会遇到问题:
SpecialInterface specialMock = mock(SpecialInterface.class);
SimpleService service = new SimpleService(specialMock);
运行时会抛出 ClassCastException。为了解决这个问题,我们可以使用 extraInterfaces()
方法让 mock 实现多个接口:
SpecialInterface specialMock = mock(SpecialInterface.class, withSettings()
.extraInterfaces(Runnable.class));
现在 mock 创建就不会失败了。不过还是那句话,❌ 强制类型转换未声明的接口不是好习惯,尽量避免。
6. 传递构造函数参数
最后一个例子中,我们来看看如何通过 MockSettings 来调用真实的构造函数并传入参数:
@Test
public void whenMockSetupWithConstructor_thenConstructorIsInvoked() {
AbstractCoffee coffeeSpy = mock(AbstractCoffee.class, withSettings()
.useConstructor("espresso")
.defaultAnswer(CALLS_REAL_METHODS));
assertEquals("Coffee name: ", "espresso", coffeeSpy.getName());
}
在这个例子中,Mockito 会尝试使用 String
类型的构造函数来创建 AbstractCoffee 的 mock 实例,并将默认行为设置为调用真实方法。
✅ 这在我们需要测试构造函数中的逻辑,或者让被测类进入特定状态时非常有用。同时,这也适用于 spy 抽象类的场景。
7. 总结
在这篇文章中,我们学习了如何通过 MockSettings 来创建更灵活的 mock 对象。
⚠️ 不过需要再次强调:虽然这些功能非常实用,有时甚至是不可避免的,但我们仍应尽量保持测试代码的简洁性,避免过度配置。
源码可以在 GitHub 上找到。