Mockito MockSettings概述

2023/05/12

1. 概述

通常,Mockito为我们的mock对象提供的默认设置应该足够我们使用。

但是,在某些情况下,我们可能需要在创建mock时提供额外的mock设置。这在调试、处理遗留代码或涉及某些边缘情况时可能很有用。

在之前的教程中,我们学习了如何使用lenient mocks。在这个教程中,我们将学习如何使用MockSettings接口提供的一些其他有用的功能。

2. MockSettings

简单地说,MockSettings接口提供了一个Fluent API,允许我们在创建mock时轻松添加和组合其他mock设置。

当我们创建一个mock对象时,所有的mock都带有一组默认设置。让我们看一个简单的mock示例:

List mockedList = mock(List.class);

在背后,Mockito mock方法委托给另一个重载方法,并为我们的mock提供了一组默认设置:

public static <T> T mock(Class<T> classToMock) {
    return mock(classToMock, withSettings());
}

让我们看看Mockito的默认设置:

public static MockSettings withSettings() {
    return new MockSettingsImpl().defaultAnswer(RETURNS_DEFAULTS);
}

正如我们所见,mock对象的标准设置非常简单。我们为mock交互配置默认Answer。通常,使用RETURNS_DEFAULTS将返回一些空值。

重要的一点是,如果需要,我们可以为我们的mock对象提供我们自己的一组自定义设置

3. 提供不同的默认Answer

现在我们对mock设置有了基本了解,让我们看看如何更改mock对象的默认返回值。

假设我们有一个非常简单的mock构建:

PizzaService service = mock(PizzaService.class);
Pizza pizza = service.orderHouseSpecial();
PizzaSize size = pizza.getSize();

当我们按预期运行此代码时,我们将得到NullPointerException,因为我们没有对orderHouseSpecial方法进行stubbing,因此它会返回null。

这是可以的,但有时在处理遗留代码时,我们可能需要处理复杂的mock对象层次结构,并且定位这些类型的异常发生的确切位置可能很耗时。

为了帮助我们解决这个问题,我们可以在mock创建期间通过mock设置提供不同的默认Answer

PizzaService pizzaService = mock(PizzaService.class, withSettings().defaultAnswer(RETURNS_SMART_NULLS));

通过使用RETURNS_SMART_NULLS作为我们的默认Answer,Mockito为我们提供了一个更有意义的错误消息,它可以准确地显示错误stubbing发生的位置:

org.mockito.exceptions.verification.SmartNullPointerException: 
You have a NullPointerException here:
-> at cn.tuyucheng.taketoday.mockito.mocksettings.MockSettingsUnitTest.whenServiceMockedWithSmartNulls_thenExceptionHasExtraInfo(MockSettingsUnitTest.java:45)
because this method call was *not* stubbed correctly:
-> at cn.tuyucheng.taketoday.mockito.mocksettings.MockSettingsUnitTest.whenServiceMockedWithSmartNulls_thenExceptionHasExtraInfo(MockSettingsUnitTest.java:44)
pizzaService.orderHouseSpecial();

这可以在调试测试代码时为我们节省一些时间。Answers枚举还提供了一些其他的预配置mock Answer:

  • RETURNS_DEEP_STUBS - 返回deep stubs的Answer,这在使用Fluent API时非常有用
  • RETURNS_MOCKS - 使用这个Answer将返回普通值,例如空集合或空字符串,然后,它将尝试返回mock。
  • CALLS_REAL_METHODS - 顾名思义,当我们使用这个实现时,unstubbed方法将委托给真正的实现。

4. 命名mock和详细日志记录

我们可以使用MockSettings的name方法为我们的mock起一个名字。这对于调试特别有用,因为我们提供的名称用于所有验证错误:

PizzaService service = mock(PizzaService.class, withSettings().name("pizzaServiceMock").verboseLogging());

在本例中,我们使用verboseLogging()方法将命名功能与详细日志记录相结合。

使用此方法可以实时记录到此mock上的方法调用的标准输出流。同样,它可以在测试调试期间使用,以发现与mock的错误交互。

当我们运行测试时,我们会在控制台上看到一些输出:

pizzaServiceMock.orderHouseSpecial();
   invoked: -> at cn.tuyucheng.taketoday.mockito.mocksettings.MockSettingsUnitTest.whenServiceMockedWithNameAndVerboseLogging_thenLogsMethodInvocations(MockSettingsUnitTest.java:37)
   has returned: "Mock for Pizza, hashCode: 1262237002" (cn.tuyucheng.taketoday.mockito.fluentapi.Pizza$MockitoMock$185391651)

有趣的是,如果我们使用@Mock注解,我们的mock会自动将字段名作为mock名称。

5. mock额外接口

有时,我们可能想要指定我们的mock应该实现的额外接口。同样,这在处理我们无法重构的遗留代码时可能很有用。

假设我们有一个特殊的接口:

public interface SpecialInterface {

}

以及使用此接口的类:

public class SimpleService {

    public SimpleService(SpecialInterface special) {
        Runnable runnable = (Runnable) special;
        runnable.run();
    }
}

当然,这不是完整的代码,但如果我们被迫为此编写单元测试,我们很可能会遇到问题:

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调用具有参数值的真实构造函数:


@ExtendWith(MockitoExtension.class)
class MockSettingsUnitTest {

    @Test
    void whenMockSetupWithConstructor_thenConstructorIsInvoked() {
        AbstractCoffee coffeeSpy = mock(AbstractCoffee.class, withSettings().useConstructor("espresso").defaultAnswer(CALLS_REAL_METHODS));

        assertEquals("espresso", coffeeSpy.getName(), "Coffee name: ");
    }
}

public abstract class AbstractCoffee {

    protected String name;

    protected AbstractCoffee(String name) {
        this.name = name;
    }

    protected String getName() {
        return name;
    }
}

这一次,Mockito在创建AbstractCoffee mock实例时尝试使用带有String值的构造函数。我们还将默认Answer配置为委托给实际实现。

如果我们在构造函数中有一些逻辑要测试或触发,以使类处于某种特定状态,那么这可能会很有用。 在spying抽象类时它也很有用。

7. 总结

在本教程中,我们了解了如何使用其他mock设置创建mock。

然而,我们强调,虽然这有时很有用并且可能是不可避免的,但在大多数情况下,我们应该使用简单的mock编写简单的测试。

与往常一样,本教程的完整源代码可在GitHub上获得。

Show Disqus Comments

Post Directory

扫码关注公众号:Taketoday
发送 290992
即可立即永久解锁本站全部文章