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库的灵活性——即使面对复杂场景也能从容应对。


原始标题:Stub Getter and Setter in Mockito | Baeldung