How to Mock Constructors for Unit Testing using Mockito – 如何使用 Mockito 为单元测试模拟构造函数

最后修改: 2023年 9月 16日

中文/混合/英文(键盘快捷键:t)

1. Introduction

1.导言

In this short tutorial, we’ll explore the various options for effectively mocking constructors in Java using Mockito and PowerMock.

在本简短教程中,我们将探讨使用 Mockito 和 PowerMock 有效模拟 Java 构造函数的各种选项。

2. Mocking Constructors Using PowerMock

2.使用 PowerMock 模拟构造函数

Mocking constructors or static methods is impossible using Mockito version 3.3 or lower. In such cases, a library like PowerMock provides additional capabilities that allow us to mock the behavior of constructors and orchestrate their interactions.

使用 Mockito 3.3 或更低版本无法模拟构造函数或静态方法。在这种情况下,PowerMock之类的库提供了额外的功能,允许我们模拟构造函数的行为并协调它们之间的交互。

3. Model

3.模型

Let’s simulate a payment processing system using two Java classes. We’ll create a PaymentService class that contains the logic for processing payments and offers the flexibility of both a parameterized constructor for specifying payment modes and a default constructor with a fallback mode:

让我们使用两个 Java 类来模拟一个支付处理系统。我们将创建一个PaymentService类,该类包含处理支付的逻辑,并提供了用于指定支付模式的参数化构造函数和具有回退模式的默认构造函数,非常灵活:

public class PaymentService {
    private final String paymentMode;
    public PaymentService(String paymentMode) {
        this.paymentMode = paymentMode;
    }

    public PaymentService() {
        this.paymentMode = "Cash";
    }

    public String processPayment(){
        return this.paymentMode;
    }
}

The PaymentProcessor class depends on the PaymentService to carry out payment processing tasks and provides two constructors, one for the default setup and another for customizing the payment mode:

PaymentProcessor类依赖于PaymentService来执行支付处理任务,并提供两个构造函数,一个用于默认设置,另一个用于自定义支付模式:

public class PaymentProcessor {
    private final PaymentService paymentService;
    public PaymentProcessor() {
        this.paymentService = new PaymentService();
    }

    public PaymentProcessor(String paymentMode) {
        this.paymentService = new PaymentService(paymentMode);
    }

    public String processPayment(){
        return paymentService.processPayment();
    }
}

4. Mocking Default Constructors Using Mockito

4.使用 Mockito 模拟默认构造函数

When writing unit tests, isolating the code we want to test is crucial. Constructors often create dependencies we don’t want to involve in our tests. Mocking constructors allow us to replace real objects with mock objects, ensuring that the behavior we’re testing is specific to the unit under examination.

在编写单元测试时,隔离我们要测试的代码至关重要。构造函数通常会创建我们不想在测试中涉及的依赖关系。通过模拟构造函数,我们可以用模拟对象替换真实对象,从而确保我们测试的行为仅限于被测试的单元。

Starting from Mockito version 3.4 and beyond, we gain access to the mockConstruction() method. It allows us to mock object constructions. We specify the class whose constructor we intend to mock as the first argument. Additionally, we provide a second argument in the form of a MockInitializer callback function. This callback function allows us to define and manipulate the behavior of the mock during construction:

从 Mockito 3.4 及更高版本开始,我们可以使用 mockConstruction() 方法。它允许我们模拟对象构造。我们指定要模拟的类的构造函数作为第一个参数此外,我们还以 MockInitializer 回调函数的形式提供了第二个参数。该回调函数允许我们在构建过程中定义和操纵 mock 的行为:

@Test
void whenConstructorInvokedWithInitializer_ThenMockObjectShouldBeCreated(){
    try(MockedConstruction<PaymentService> mockPaymentService = Mockito.mockConstruction(PaymentService.class,(mock,context)-> {
        when(mock.processPayment()).thenReturn("Credit");
    })){
        PaymentProcessor paymentProcessor = new PaymentProcessor();
        Assertions.assertEquals(1,mockPaymentService.constructed().size());
        Assertions.assertEquals("Credit", paymentProcessor.processPayment());
    }
}

There are several overloaded versions of the mockConstruction() method, each catering to different use cases. In the scenario below, we don’t use the MockInitializer to initialize the mock object. We’re verifying that the constructor was called once, and the absence of an initializer ensures the paymentMode field’s null state in the constructed PaymentService object:

mockConstruction()方法有多个重载版本,每个版本都针对不同的使用情况。在下面的场景中,我们没有使用 MockInitializer 来初始化模拟对象。我们正在验证构造函数是否被调用过一次,而初始化器的缺失将确保 PaymentMode 字段在构建的 PaymentService 对象中处于 null 状态:

@Test
void whenConstructorInvokedWithoutInitializer_ThenMockObjectShouldBeCreatedWithNullFields(){
    try(MockedConstruction<PaymentService> mockPaymentService = Mockito.mockConstruction(PaymentService.class)){
        PaymentProcessor paymentProcessor = new PaymentProcessor();
        Assertions.assertEquals(1,mockPaymentService.constructed().size());
        Assertions.assertNull(paymentProcessor.processPayment());
    }
}

5. Mocking Parameterized Constructors Using Mockito

5.使用 Mockito 模拟参数化构造函数

In this example, we’ve set up the MockInitializer and invoked the parameterized constructor. We’re verifying that there is precisely one mock created, and it has the desired value defined during initialization:

在本例中,我们设置了 MockInitializer 并调用了参数化构造函数。我们正在验证是否创建了一个精确的 mock,并且它在初始化过程中定义了所需的值:

@Test
void whenConstructorInvokedWithParameters_ThenMockObjectShouldBeCreated(){
    try(MockedConstruction<PaymentService> mockPaymentService = Mockito.mockConstruction(PaymentService.class,(mock, context) -> {
        when(mock.processPayment()).thenReturn("Credit");
    })){
        PaymentProcessor paymentProcessor = new PaymentProcessor("Debit");
        Assertions.assertEquals(1,mockPaymentService.constructed().size());
        Assertions.assertEquals("Credit", paymentProcessor.processPayment());
    }
}

6. Scope of the Mocked Constructor

6.模拟构造函数的范围

The try-with-resources construct in Java allows us to limit the scope of the mock being created. Within this block, any invocation of public constructors for the specified class creates mock objects. The real constructor will be invoked when it is called anywhere outside the block.

Java 中的 try-with-resources 构造允许我们限制所创建 mock 的范围。在该代码块中,对指定类的公共构造函数的任何调用都会创建 mock 对象。在该代码块之外的任何地方调用真正的构造函数时,都将调用该构造函数。

In the below example, we don’t define any initializer and invoke both the default and parameterized constructors multiple times. The behavior of the mock is then defined post-construction.

在下面的示例中,我们没有定义任何初始化器,而是多次调用默认构造函数和参数化构造函数。然后在构造后定义 mock 的行为。

We’re verifying that three mock objects have indeed been created and are adhering to our predefined mock behavior:

我们正在验证是否确实创建了三个 mock 对象,并且它们是否符合我们预定义的 mock 行为:

@Test
void whenMultipleConstructorsInvoked_ThenMultipleMockObjectsShouldBeCreated(){
    try(MockedConstruction<PaymentService> mockPaymentService = Mockito.mockConstruction(PaymentService.class)){
        PaymentProcessor paymentProcessor = new PaymentProcessor();
        PaymentProcessor secondPaymentProcessor = new PaymentProcessor();
        PaymentProcessor thirdPaymentProcessor = new PaymentProcessor("Debit");

        when(mockPaymentService.constructed().get(0).processPayment()).thenReturn("Credit");
        when(mockPaymentService.constructed().get(1).processPayment()).thenReturn("Online Banking");

        Assertions.assertEquals(3,mockPaymentService.constructed().size());
        Assertions.assertEquals("Credit", paymentProcessor.processPayment());
        Assertions.assertEquals("Online Banking", secondPaymentProcessor.processPayment());
        Assertions.assertNull(thirdPaymentProcessor.processPayment());
    }
}

7. Dependency Injection and Constructor Mocking

7.依赖注入和构造函数模拟

When we use dependency injection, we can directly pass mock objects, avoiding the need to mock constructors. With this approach, we can mock the dependency before instantiating the class under test, eliminating the need to mock any constructors.

当我们使用依赖注入时,我们可以直接传递模拟对象,而无需模拟构造函数。通过这种方法,我们可以在实例化被测试类之前模拟依赖关系,从而无需模拟任何构造函数。

Let’s introduce a third constructor in the PaymentProcessor class where the PaymentService is injected as a dependency:

让我们在 PaymentProcessor 类中引入第三个构造函数,将 PaymentService 作为依赖注入其中:

public PaymentProcessor(PaymentService paymentService) {
    this.paymentService = paymentService;
}

We have decoupled the dependency from the PaymentProcessor class, which allows us to test our unit in isolation and also control the behavior of the dependency through mocks, as shown below:

我们将依赖关系与 PaymentProcessor 类解耦,这样就可以隔离测试我们的单元,并通过模拟控制依赖关系的行为,如下所示:

@Test
void whenDependencyInjectionIsUsed_ThenMockObjectShouldBeCreated(){
    PaymentService mockPaymentService = Mockito.mock(PaymentService.class);
    when(mockPaymentService.processPayment()).thenReturn("Online Banking");
    PaymentProcessor paymentProcessor = new PaymentProcessor(mockPaymentService);
    Assertions.assertEquals("Online Banking", paymentProcessor.processPayment());
}

However, in situations where we can’t control how dependencies are managed in the source code, especially when dependency injection isn’t an option, mockConstruction() becomes a useful tool for effectively mocking constructors.

但是,在我们无法控制如何在源代码中管理依赖关系的情况下,尤其是在无法选择依赖注入的情况下,mockConstruction() 成为了有效模拟构造函数的有用工具。

8. Conclusion

8.结论

This brief article has shown different ways to mock constructors through Mockito and PowerMock. We’ve also discussed the advantages of prioritizing dependency injection when feasible.

本文简要介绍了通过 Mockito 和 PowerMock 模拟构造函数的不同方法。我们还讨论了在可行的情况下优先考虑依赖注入的优势。

As always, the code is available over on GitHub.

与往常一样,代码可在 GitHub 上获取。