JUnit 5 @ParameterizedTest注解详解

后端 潘老师 2年前 (2023-10-23) 211 ℃ (0) 扫码查看

参数化测试用于多次运行相同的测试方法,但使用不同的输入数据集。当我们需要针对多个不同的输入值测试功能时,这非常有用。与其编写多个测试方法来处理不同的输入情况,不如将参数化测试将这些测试案例整合到一个单一的方法中。

JUnit 5的@ParameterizedTest注解允许我们以干净和组织的方式编写参数化测试。让我们了解它的目的、如何使用它及其优点。

1.设置

要使用参数化测试,请在项目中包含junit-jupiter-params依赖项。

<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-params</artifactId>
    <version>5.8.1</version>
    <scope>test</scope>
</dependency>

2.@ParameterizedTest示例

使用@ParameterizedTest注解来多次执行测试,但使用不同的参数。
我们不需要使用@Test注解,相反,只需要在测试方法上使用@ParameterizedTest注解。

//语法
@ParameterizedTest(...)
@ValueSource(...)
void testMethod(String param) {
  //...
}

我们必须声明至少一个参数源,为每次调用提供参数,供测试方法使用。

在给定的示例中,testPalindrome将针对@ValueSource注解中的每个字符串执行2次。我们可以使用方法参数word来访问参数。使用@ParameterizedTest注解中的name参数来自定义显示消息。

public class ParameterizedTests {
    @ParameterizedTest(name = "{index} - {0} is a palindrome")
    @ValueSource(strings = { "12321", "pop" })
    void testPalindrome(String word) {
        assertTrue(word == null ? false : StringUtils.reverse(word).equals(word));
    }
}

3. 参数提供者

JUnit 5使用ArgumentsProvider类来为测试提供输入参数。您可以实现自定义ArgumentsProvider或使用JUnit 5提供的内置提供程序。

让我们首先了解内置提供程序。

参数提供者 描述
@ValueSource 用于简单的字面值,如原始类型和字符串。不支持将null作为值。
@NullSource 用于单一null参数。
@EmptySource 用于单一空参数,类型为String、List、Set、Map或数组。
@NullAndEmptySource 用于一个null值,然后是一个空值。
@EnumSource 测试方法每次针对一个枚举常量进行调用。
@MethodSource 用于一个或多个生成参数流的工厂方法。
@CsvSource 用于逗号分隔的参数列表。
@CsvFileSource 用于从文件读取CSV标记。
@ArgumentsSource 用于自定义和可重复使用的ArgumentsProvider。
@ValueSource注解不支持null作为值。因此,通过在@ValueSource中使用@NullSource和@EmptySource,我们可以在同一个测试中测试null、非null和空值。

让我们更详细地了解每个参数提供者。

3.1. @ValueSource

对于简单的字面值,如原始类型和字符串,使用@ValueSource。

  • 它指定一个单一的值数组,并且只能用于为每个参数化的测试调用提供一个单一的参数。
  • Java支持自动装箱,因此我们也可以使用包装类来接收字面值。
  • 我们不能将null作为参数传递,即使是String和Class类型。
@ParameterizedTest
@ValueSource(ints = { 1, 2, 3 })
void testMethod(int argument) {
    //test code
}
@ParameterizedTest
@ValueSource(ints = { 1, 2, 3 })
void testMethodWithAutoboxing(Integer argument) {
    //test code
}

3.2. @NullSource

它为注解的@ParameterizedTest方法提供一个单一的null参数。

@ParameterizedTest
@NullSource
void testMethodNullSource(Integer argument) {
    assertTrue(argument == null);
}

3.3. @EmptySource

它为注解的@ParameterizedTest方法提供一个单一的空参数,其类型如下:

  • java.lang.String
  • java.util.List
  • java.util.Set
  • java.util.Map
  • primitive arrays (比如 int[])
  • object arrays (比如 String[])
@ParameterizedTest
@EmptySource
void testMethodEmptySource(String argument) {
    assertTrue(StringUtils.isEmpty(argument));
}

3.4. @NullAndEmptySource

它结合了@NullSource和@EmptySource的功能。在给定示例中,测试方法将两次被调用,首先是使用null值,然后使用空值。

@ParameterizedTest
@NullAndEmptySource
void testMethodNullAndEmptySource(String argument) {
    assertTrue(StringUtils.isEmpty(argument));
}

3.5@EnumSource

它提供了一个方便的方式来使用枚举常量。测试方法将每次为枚举常量中的一个被调用。在给定示例中,测试方法将四次被调用,每次针对一个枚举常量。

enum Direction {
    EAST, WEST, NORTH, SOUTH
}
@ParameterizedTest
@EnumSource(Direction.class)
void testWithEnumSource(Direction d) {
    assertNotNull(d);
}

3.6 @MethodSource

  • 它用于引用测试类或外部类的单个或多个工厂方法。工厂方法必须生成参数流,其中每个参数流将被注解的@ParameterizedTest方法消耗。
  • 除非测试类被注解为@TestInstance(Lifecycle.PER_CLASS),否则工厂方法必须是静态的。
  • 此外,工厂方法不能接受任何方法参数。
@ParameterizedTest
@MethodSource("argsProviderFactory")
void testWithMethodSource(String argument) {
    assertNotNull(argument);
}
static Stream<String> argsProviderFactory() {
    return Stream.of("alex", "brian");
}

如果我们不通过@MethodSource显式提供一个工厂方法名,JUnit将默认搜索与当前@ParameterizedTest方法同名的工厂方法。

因此,在给定示例中,如果我们不在@MethodSource注释中提供方法名argsProviderFactory,JUnit将搜索名为testWithMethodSource的方法,该方法返回类型为Stream。

@ParameterizedTest
@MethodSource
void testWithMethodSource(String argument) {
    assertNotNull(argument);
}
static Stream<String> testWithMethodSource() {
    return Stream.of("alex", "brian");
}

还支持原始类型流(DoubleStream、IntStream和LongStream)。

@ParameterizedTest
@MethodSource("argsProviderFactory")
void testWithMethodSource(int argument) {
    assertNotEquals(9, argument);
}
static IntStream argsProviderFactory() {
    return IntStream.range(0, 10);
}

3.7. @CsvSource

它允许我们将参数列表表示为逗号分隔的值。每个CSV令牌代表CSV行,并导致参数化测试的一次调用。设置属性ignoreLeadingAndTrailingWhitespace为true或false,这会提示JUnit保留或忽略CSV令牌中的空格。

@ParameterizedTest
@CsvSource(value = {
    "alex, 30",
    "brian, 35",
    "charles, 40"
}, ignoreLeadingAndTrailingWhitespace = true)
void testWithCsvSource(String name, int age) {
    assertNotNull(name);
    assertTrue(age > 0);
}

3.8@CsvFileSource

它与@CsvSource非常相似,但是我们是从文件中读取CSV令牌而不是读取内联令牌。CSV文件可以从类路径或本地文件系统中读取。默认分隔符是一个逗号(,),但我们可以使用另一个字符通过设置delimiter属性。注意,任何以#符号开头的行将被解释为注释并被忽略。

@ParameterizedTest
@CsvFileSource(resources = "employeeData.csv", numLinesToSkip = 0)
void testWithCsvFileSource(String name, int age) {
    assertNotNull(name);
    assertTrue(age > 0);
}

3.9 @ArgumentsSource

@ArgumentsSource可用于指定自定义的、可重复使用的ArgumentsProvider。

@ParameterizedTest(name = "{index} - {0} is older than 40")
@ArgumentsSource(EmployeesArgumentsProvider.class)
void isEmployeeAgeGreaterThan40(Employee e) {
    assertTrue(Period.between(e.getDob(), LocalDate.now()).get(ChronoUnit.YEARS) > 40);
}
class EmployeesArgumentsProvider implements ArgumentsProvider {
    @Override
    public Stream<? extends Arguments> provideArguments(ExtensionContext context) {
        return Stream.of(
          Arguments.of(new Employee(1, "Alex", LocalDate.of(1980, 2, 3))),
          Arguments.of(new Employee(2, "Brian", LocalDate.of(1979, 2, 3))),
          Arguments.of(new Employee(3, "Charles", LocalDate.of(1978, 2, 3)))
        );
    }
}

4. 带有多个参数的参数化测试

要编写可以接受多个参数的测试,我们可以使用以下注释:

4.1 @CsvSource

如前文第3.7节所示,我们可以使用@CsvSource注释提供许多文字和简单类型的参数。我们需要将所有参数提供在一个CSV令牌中,然后定义匹配的方法参数。

@ParameterizedTest
@CsvSource({
    "alex, 30, HR, Active",
    "brian, 35, Technology, Active",
    "charles, 40, Finance, Purged"
})
void testWithCsvSource(String name, int age, String department, String status) {
    //test code
}

4.2. Custom ArgumentsProvider

为了提供复杂或自定义类型的多个测试参数,我们应该使用带有ArgumentsProvider注释的@ArgumentsSource注释。

在给定的示例中,我们将三个参数传递给名为testArgumentsSource的测试方法,分别是Employee类型、LocalDate类型和Direction类型的枚举常量。

@ParameterizedTest
@ArgumentsSource(EmployeesArgumentsProvider.class)
void testArgumentsSource(Employee e, LocalDate date, Direction d) {
    // Use arguments to test functionality
    //...
    assertTrue(Period.between(e.getDob(), LocalDate.now()).get(ChronoUnit.YEARS) > 40);
    assertNotNull(date);
    assertNotNull(d);
}
class EmployeesArgumentsProvider implements ArgumentsProvider {
    @Override
    public Stream<? extends Arguments> provideArguments(ExtensionContext context) {
        return Stream.of(
          Arguments.of(new Employee(1, "Alex", LocalDate.of(1980, 2, 3)), LocalDate.now(), Direction.EAST),
          Arguments.of(new Employee(2, "Brian", LocalDate.of(1979, 2, 3)), LocalDate.now(), Direction.NORTH),
          Arguments.of(new Employee(3, "Charles", LocalDate.of(1978, 2, 3)), LocalDate.now(), Direction.SOUTH)
        );
    }

5. 结论

JUnit 5 @ParameterizedTest注释对于编写必须多次调用但带有不同参数进行测试的测试非常有帮助。JUnit提供了多种声明式方法来为测试方法提供参数。

我们还可以将不同的注释组合起来,以在单个测试中测试各种类型的输入。

还有一些更复杂的概念,如参数聚合器、参数转换器等。请阅读官方JUnit文档以获取最新信息。


版权声明:本站文章,如无说明,均为本站原创,转载请注明文章来源。如有侵权,请联系博主删除。
本文链接:https://www.panziye.com/back/10285.html
喜欢 (0)
请潘老师喝杯Coffee吧!】
分享 (0)
用户头像
发表我的评论
取消评论
表情 贴图 签到 代码

Hi,您需要填写昵称和邮箱!

  • 昵称【必填】
  • 邮箱【必填】
  • 网址【可选】