1. 概述

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

2. 使用PowerMock模拟构造函数

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

3. 模型

让我们用两个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;
    }
}

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. 使用Mockito模拟默认构造函数

编写单元测试时,隔离要测试的代码至关重要。 构造函数通常会创建我们不想在测试中涉及的依赖。模拟构造函数允许我们将真实对象替换为模拟对象,确保我们测试的是特定单元的行为。

从Mockito 3.4及更高版本开始,我们可以访问mockConstruction()方法。它允许我们模拟对象的构造。我们首先指定要模拟构造函数的类作为第一个参数其次,我们提供一个形式为MockInitializer回调函数的第二个参数。这个回调函数允许我们在构造过程中定义和操纵模拟对象的行为:

@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());
    }
}

mockConstruction()方法有多个重载版本,以适应不同的用例。在下面的例子中,我们没有使用MockInitializer初始化模拟对象。我们验证构造函数被调用了一次,而没有初始化器确保构建的PaymentService对象中的paymentMode字段处于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. 使用Mockito模拟参数化构造函数

在这个例子中,我们设置了MockInitializer并调用了参数化构造函数。我们验证确实创建了一个精确的模拟对象,并且在初始化时具有期望的值:

@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. 模拟构造函数的范围

在Java中,我们可以使用try-with-resources语句来限制模拟对象的范围。在这个块内,对指定类的公共构造函数的任何调用都会创建模拟对象。当块外的任何地方调用真正的构造函数时,它会被调用。

以下示例中,我们没有定义任何初始化器,并多次调用默认和参数化构造函数。然后,模拟行为在构造后定义:

我们验证确实创建了三个模拟对象,并且遵循我们的预定义模拟行为:

@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. 依赖注入与构造函数模拟

当我们使用依赖注入时,可以直接传递模拟对象,无需模拟构造函数。通过这种方法,我们可以在测试类之前模拟依赖项,从而消除模拟构造函数的需要。

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

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

我们已经解耦了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());
}

然而,在源代码中无法控制依赖管理的情况下,特别是当依赖注入不可用时,mockConstruction()仍然是有效模拟构造函数的有用工具。

8. 总结

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

如往常一样,代码可在GitHub上找到。