1. 简介
虽然Mockito提供了避免初始化非必要对象的优秀方案,但有时其开箱即用的功能会存在限制。本教程将探索在单元测试场景下处理getter和setter存根的各种方法。
2. 模拟对象 vs 实际对象
编写测试前,先理解存根和模拟对象的区别:存根对象的行为是固定且不可配置的,而模拟对象允许自定义行为并具备验证能力。
为贴近真实场景,我们定义ExampleService
类,它调用其他对象的方法(无论是否存根):
public class ExampleService {
public <T> T getField(Supplier<T> getter) {
return getter.get();
}
public <T> void setField(Consumer<T> setter, T value) {
setter.accept(value);
}
}
ExampleService
的设计注重复用性,因此getField()
和setField()
方法都接受函数式参数。简单说:getField()
调用传入的Supplier
(即getter方法),而setField()
调用传入的Consumer
(即setter方法)并传入指定值。
为清晰演示,以下是使用ExampleService
调用getter/setter的示例:
exampleService.getField(() -> fooBar.getFoo()); // 调用getFoo getter
exampleService.getField(fooBar::getBar); // 调用getBar getter
exampleService.setField((bar) -> fooBar.setBar(bar), "newBar"); // 调用bar setter
exampleService.setField(fooBar::setBar, "newBar"); // 调用bar setter
测试前最后一步是定义模型类SimpleClass
:
public class SimpleClass {
private Long id;
private String name;
// getters, setters, constructors
⚠️ 踩坑提醒:对于轻量级、易初始化的对象,使用模拟对象往往得不偿失。模拟代码比直接创建实例更冗长。以下对比存根版本和真实对象的测试:
存根版本测试(8行额外代码):
@Test
public void givenMockedSimpleClass_whenInvokingSettersGetters_thenInvokeMockedSettersGetters() {
Long mockId = 12L;
String mockName = "I'm 12";
SimpleClass simpleMock = mock(SimpleClass.class);
when(simpleMock.getId()).thenReturn(mockId);
when(simpleMock.getName()).thenReturn(mockName);
doNothing().when(simpleMock).setId(anyLong());
doNothing().when(simpleMock).setName(anyString());
ExampleService srv = new ExampleService();
srv.setField(simpleMock::setId, 11L);
srv.setField(simpleMock::setName, "I'm 11");
assertEquals(srv.getField(simpleMock::getId), mockId);
assertEquals(srv.getField(simpleMock::getName), mockName);
verify(simpleMock).getId();
verify(simpleMock).getName();
verify(simpleMock).setId(eq(11L));
verify(simpleMock).setName(eq("I'm 11"));
}
真实对象版本(简洁高效):
@Test
public void givenActualSimpleClass_whenInvokingSettersGetters_thenInvokeActualSettersGetters() {
Long id = 1L;
String name = "I'm 1";
SimpleClass simple = new SimpleClass(id, name);
ExampleService srv = new ExampleService();
srv.setField(simple::setId, 2L);
srv.setField(simple::setName, "I'm 2");
assertEquals(srv.getField(simple::getId), simple.getId());
assertEquals(srv.getField(simple::getName), simple.getName());
}
✅ 关键结论:模拟对象的存根设置和验证会导致测试代码膨胀,对简单对象应优先使用真实实例。
3. 基础模拟对象
当对象创建需要大量代码或初始化缓慢(影响测试性能)时,Mockito存根才是最佳选择。为此引入更复杂的NonSimpleClass
:
public class NonSimpleClass {
private Long id;
private String name;
private String superComplicatedField;
// getters, setters, constructors
顾名思义,superComplicatedField
需要特殊处理,测试中必须避免初始化它:
@Test
public void givenNonSimpleClass_whenInvokingGetName_thenReturnMockedName() {
NonSimpleClass nonSimple = mock(NonSimpleClass.class);
when(nonSimple.getName()).thenReturn("Meredith");
ExampleService srv = new ExampleService();
assertEquals(srv.getField(nonSimple::getName), "Meredith");
verify(nonSimple).getName();
}
这种场景下,创建NonSimpleClass
实例会导致性能问题或冗余代码。而Mockito存根既能覆盖测试需求,又避免了实例化带来的副作用。
4. 有状态模拟对象
Mockito默认不管理对象状态。setter设置的值不会被后续getter调用返回,除非手动处理。解决方案是创建有状态模拟对象:当getter在setter后调用时,返回最新设置的值。通过Wrapper
类管理状态:
class Wrapper<T> {
private T value;
// getter, setter, constructors
使用Wrapper
时,需用doAnswer()
和thenAnswer()
替代thenReturn()
和doNothing()
。这些方法允许访问模拟方法的参数并执行自定义逻辑:
@Test
public void givenNonSimpleClass_whenInvokingGetName_thenReturnTheLatestNameSet() {
Wrapper<String> nameWrapper = new Wrapper<>(String.class);
NonSimpleClass nonSimple = mock(NonSimpleClass.class);
when(nonSimple.getName()).thenAnswer((Answer<String>) invocationOnMock -> nameWrapper.get());
doAnswer(invocation -> {
nameWrapper.set(invocation.getArgument(0));
return null;
}).when(nonSimple)
.setName(anyString());
ExampleService srv = new ExampleService();
srv.setField(nonSimple::setName, "John");
assertEquals(srv.getField(nonSimple::getName), "John");
srv.setField(nonSimple::setName, "Nick");
assertEquals(srv.getField(nonSimple::getName), "Nick");
}
✅ 实现原理:有状态模拟对象通过底层Wrapper
实例保存setter的最新值,并在getter被调用时返回该值。
5. 总结
本文探讨了多种模拟场景,明确了何时使用模拟对象(如复杂对象初始化)以及何时避免(如简单对象)。同时展示了有状态模拟对象的解决方案,体现了Mockito库的灵活性——即使面对复杂场景也能从容应对。