JUnit 参数化测试中 Mock 对象返回参数化值的正确方法

本文介绍了在使用 JUnit 参数化测试和 Mockito 框架时,如何正确地配置和使用 Mock 对象,使其能够根据参数化测试的输入参数返回不同的值。重点在于 runner 的选择,通过使用 MockitoExtension 解决了 InvalidUseOfMatchersException 异常,并提供了一个可运行的示例代码。

在使用 JUnit 进行单元测试时,参数化测试可以方便地使用不同的输入数据运行相同的测试逻辑。结合 Mockito 框架,我们可以模拟外部依赖的行为,从而更好地隔离被测代码。然而,在参数化测试中,如果 Mock 对象的行为依赖于参数化测试的输入,可能会遇到一些问题。本文将介绍如何正确地配置和使用 Mock 对象,使其能够根据参数化测试的输入参数返回不同的值。

问题分析

在使用 Mockito 时,常见的错误之一是 InvalidUseOfMatchersException,这通常发生在参数匹配器(如 any())被错误地使用时。例如,在没有进行 stu*g 或 verification 的情况下使用了参数匹配器。

解决方案

解决此问题的关键在于正确地配置 JUnit 运行器和 Mockito 扩展。以下是推荐的步骤:

  1. 使用 MockitoExtension

    确保你的测试类使用了 MockitoExtension 作为 JUnit 的扩展。这可以通过在类上添加 @ExtendWith(MockitoExtension.class) 注解来实现。MockitoExtension 负责初始化 Mockito 的 Mock 对象,并处理相关的生命周期。

    @ExtendWith(MockitoExtension.class)
    public class FooTest {
    
        @Mock
        MockedObject mockedObject;
    
        @InjectMocks
        Foo underTest;
    
        // ...
    }

    注意:这里使用了 @Mock 注解,它与 @Mocked 注解功能类似,都是用来创建 Mock 对象的。但 @Mock 是 Mockito 提供的标准注解,建议使用它。

  2. 参数化测试数据提供

    使用 @MethodSource 注解指定一个提供参数化测试数据的静态方法。该方法返回一个 Stream,其中每个 Arguments 对象包含测试方法的输入参数。

    @ParameterizedTest
    @MethodSource("dataProvider")
    public void test_ParametrizedTest(MockedInput mockedInput

    , Output expectedReturn) { // Given when(mockedObject.method(mockedInput)) .thenReturn(expectedReturn); // when val result = underTest.method(); // then assertEquals(expectedReturn.getCode(), result.getCode()); } private static Stream dataProvider() { MockedInput mockedInput1 = new MockedInput("S1"); MockedInput mockedInput2 = new MockedInput("S2"); return Stream.of( Arguments.of(mockedInput1, Output.builder().code(CodeEnum.S1).build()), Arguments.of(mockedInput2, Output.builder().code(CodeEnum.S2).build()) ); }

    在这个例子中,dataProvider 方法返回一个包含 MockedInput 和 Output 对象的 Stream。每个 Arguments 对象都对应一个测试用例。

  3. Mock 对象的 Stu*g

    在测试方法中,使用 when(mockedObject.method(mockedInput)).thenReturn(expectedReturn) 来指定 Mock 对象的行为。这里,mockedInput 是参数化测试的输入参数,expectedReturn 是期望的返回值。Mockito 会根据传入的 mockedInput 参数,返回相应的 expectedReturn 值。

  4. 断言

    最后,使用 assertEquals 或其他断言方法来验证被测代码的行为是否符合预期。

完整示例

以下是一个完整的示例代码,展示了如何在 JUnit 参数化测试中使用 Mockito 模拟对象,并根据不同的输入参数返回不同的值:

import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.mockito.Mockito.when;
import static org.junit.jupiter.api.Assertions.assertEquals;

import lombok.Builder;
import lombok.Data;
import lombok.Value;

import java.util.stream.Stream;

@ExtendWith(MockitoExtension.class)
public class FooTest {

    @Mock
    MockedObject mockedObject;

    @InjectMocks
    Foo underTest;

    @ParameterizedTest
    @MethodSource("dataProvider")
    public void test_ParametrizedTest(MockedInput mockedInput, Output expectedReturn) {

        // Given
        when(mockedObject.method(mockedInput))
            .thenReturn(expectedReturn);

        // when
        Output result = underTest.method(mockedInput);

        // then
        assertEquals(expectedReturn.getCode(), result.getCode());

    }

    private static Stream dataProvider() {
        MockedInput mockedInput1 = new MockedInput("S1");
        MockedInput mockedInput2 = new MockedInput("S2");
        return Stream.of(
            Arguments.of(mockedInput1, Output.builder().code(CodeEnum.S1).build()),
            Arguments.of(mockedInput2, Output.builder().code(CodeEnum.S2).build())
        );
    }

    // 辅助类
    public static enum CodeEnum {
        S1("S1"),
        S2("S2");

        private String value;

        CodeEnum(String value) {
            this.value = value;
        }

        public String getValue() {
            return value;
        }
    }

    @Value
    public static class MockedInput {
        String input;
    }

    @Builder
    @Data
    public static class Output {
        CodeEnum code;
    }

    public static class Foo {
        private MockedObject mockedObject;

        public Foo(MockedObject mockedObject) {
            this.mockedObject = mockedObject;
        }

        public Output method(MockedInput input) {
            return mockedObject.method(input);
        }
    }

    public static class MockedObject {
        public Output method(MockedInput input) {
            // This method will be mocked
            return null;
        }
    }
}

注意事项

  • 确保你的 JUnit 和 Mockito 版本兼容。
  • 避免在参数化测试中使用过于复杂的参数匹配器,尽量使用具体的参数值进行 Mock 对象的 stu*g。
  • 如果遇到 InvalidUseOfMatchersException,首先检查是否正确地使用了 MockitoExtension,并确保参数匹配器只在 stu*g 或 verification 中使用。

总结

通过使用 MockitoExtension 和 @MethodSource 注解,我们可以轻松地在 JUnit 参数化测试中使用 Mockito 模拟对象,并根据不同的输入参数返回不同的值。这种方法可以有效地隔离被测代码,并提高单元测试的质量。