JUnit 5参数化测试

目录

  • 设置
  • 咱们的第一个参数化测试
  • 参数起源
    • @ValueSource
    • @NullSource & @EmptySource
    • @MethodSource
    • @CsvSource
    • @CsvFileSource
    • @EnumSource
    • @ArgumentsSource
    • 参数转换
    • 参数聚合
  • 处分
  • 总结

如果您正在浏览这篇文章,阐明您曾经相熟了JUnit。让我为您概括一下JUnit——在软件开发中,咱们开发人员编写的代码可能是设计一个人的个人资料这样简略,也可能是在银行零碎中进行付款这样简单。在开发这些性能时,咱们偏向于编写单元测试。顾名思义,单元测试的次要目标是确保代码的小、独自局部按预期性能工作。如果单元测试执行失败,这意味着该性能无奈按预期工作。编写单元测试的一种工具是JUnit。这些单元测试程序很小,然而十分弱小,并且能够疾速执行。如果您想理解更多对于JUnit 5(也称为JUnit Jupiter)的信息,请查看这篇JUnit5的文章。当初咱们曾经理解了JUnit,接下来让咱们聚焦于JUnit 5中的参数化测试。参数化测试能够解决在为任何新/旧性能开发测试框架时遇到的最常见问题。

  • 编写针对每个可能输出的测试用例变得更加容易。
  • 单个测试用例能够承受多个输出来测试源代码,有助于缩小代码反复。
  • 通过应用多个输出运行单个测试用例,咱们能够确信已涵盖所有可能的场景,并保护更好的代码覆盖率。

开发团队通过利用办法和类来创立可重用且涣散耦合的源代码。传递给代码的参数会影响其性能。例如,计算器类中的sum办法能够解决整数和浮点数值。JUnit 5引入了执行参数化测试的能力,能够应用单个测试用例测试源代码,该测试用例能够承受不同的输出。这样能够更无效地进行测试,因为在旧版本的JUnit中,必须为每种输出类型创立独自的测试用例,从而导致大量的代码反复。

示例代码

本文附带有在 GitHub上 的一个可工作的示例代码。

设置

就像疯狂泰坦灭霸喜爱拜访力量一样,您能够应用以下Maven依赖项来拜访JUnit5中参数化测试的力量:

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

让咱们来写些代码,好吗?

咱们的第一个参数化测试

当初,我想向您介绍一个新的注解 @ParameterizedTest。顾名思义,它通知JUnit引擎应用不同的输出值运行此测试。

import static org.junit.jupiter.api.Assertions.assertEquals;import org.junit.jupiter.params.ParameterizedTest;import org.junit.jupiter.params.provider.ValueSource;public class ValueSourceTest {    @ParameterizedTest    @ValueSource(ints = { 2, 4 })    void checkEvenNumber(int number) {        assertEquals(0, number % 2,         "Supplied number is not an even number");    }}

在下面的示例中,注解@ValueSource为 checkEvenNumber() 办法提供了多个输出。假如咱们应用JUnit4编写雷同的代码,即便它们的后果(断言)完全相同,咱们也必须编写2个测试用例来笼罩输出2和4。

当咱们执行 ValueSourceTest 时,咱们会看到什么:

ValueSourceTest

|_ checkEvenNumber

|_ [1] 2

|_ [2] 4

这意味着 checkEvenNumber() 办法将应用2个输出值执行。

在下一节中,让咱们学习一下JUnit5框架提供的各种参数起源。

参数起源

JUnit5提供了许多参数起源正文。上面的章节将简要概述其中一些正文并提供示例。

@ValueSource

@ValueSource是一个简略的参数源,能够承受单个字面值数组。@ValueSource反对的字面值类型有short、byte、int、long、float、double、char、boolean、String和Class。

@ParameterizedTest@ValueSource(strings = { "a1", "b2" })void checkAlphanumeric(String word) {    assertTrue(StringUtils.isAlphanumeric(word),             "Supplied word is not alpha-numeric");}

@NullSource & @EmptySource

假如咱们须要验证用户是否曾经提供了所有必填字段(例如在登录函数中须要提供用户名和明码)。咱们应用注解来查看提供的字段是否为 null,空字符串或空格。

  • 在单元测试中应用 @NullSource 和 @EmptySource 能够帮忙咱们提供带有 null、空字符串和空格的数据源,并验证源代码的行为。
@ParameterizedTest@NullSourcevoid checkNull(String value) {    assertEquals(null, value);}@ParameterizedTest@EmptySourcevoid checkEmpty(String value) {    assertEquals("", value);}
  • 咱们还能够应用 @NullAndEmptySource 注解来组合传递 null 和空输出。
@ParameterizedTest@NullAndEmptySourcevoid checkNullAndEmpty(String value) {    assertTrue(value == null || value.isEmpty());}
  • 另一个传递 null、空字符串和空格输出值的技巧是联合应用 @NullAndEmptySource 注解,以笼罩所有可能的负面状况。该注解容许咱们从一个或多个测试类的工厂办法中加载输出,并生成一个参数流。
@ParameterizedTest@NullAndEmptySource@ValueSource(strings = { " ", " " })void checkNullEmptyAndBlank(String value) {    assertTrue(value == null || value.isBlank());}

@MethodSource

该注解容许咱们从一个或多个测试类的工厂办法中加载输出,并生成一个参数流。

  • 显式办法源 - 测试将尝试加载提供的办法。
// Note: The test will try to load the supplied method@ParameterizedTest@MethodSource("checkExplicitMethodSourceArgs")void checkExplicitMethodSource(String word) {assertTrue(StringUtils.isAlphanumeric(word),"Supplied word is not alpha-numeric");}static Stream<String> checkExplicitMethodSourceArgs() {return Stream.of("a1","b2");}
  • 隐式办法源 - 测试将搜寻与测试类匹配的源办法。
// Note: The test will search for the source method// that matches the test-case method name@ParameterizedTest@MethodSourcevoid checkImplicitMethodSource(String word) {    assertTrue(StringUtils.isAlphanumeric(word),"Supplied word is not alpha-numeric");}static Stream<String> checkImplicitMethodSource() {return Stream.of("a1","b2");}
  • 多参数办法源 - 咱们必须将输出作为参数流传递。测试将依照索引程序加载参数。
// Note: The test will automatically map arguments based on the index@ParameterizedTest@MethodSourcevoid checkMultiArgumentsMethodSource(int number, String expected) {    assertEquals(StringUtils.equals(expected, "even") ? 0 : 1, number % 2);}static Stream<Arguments> checkMultiArgumentsMethodSource() {    return Stream.of(Arguments.of(2, "even"),     Arguments.of(3, "odd"));}
  • 内部办法源 - 测试将尝试加载内部办法。
// Note: The test will try to load the external method@ParameterizedTest@MethodSource("source.method.ExternalMethodSource#checkExternalMethodSourceArgs")void checkExternalMethodSource(String word) {    assertTrue(StringUtils.isAlphanumeric(word),"Supplied word is not alpha-numeric");}// Note: The test will try to load the external method@ParameterizedTest@MethodSource("source.method.ExternalMethodSource#checkExternalMethodSourceArgs")void checkExternalMethodSource(String word) {    assertTrue(StringUtils.isAlphanumeric(word),"Supplied word is not alpha-numeric");}package source.method;import java.util.stream.Stream;public class ExternalMethodSource {    static Stream<String> checkExternalMethodSourceArgs() {        return Stream.of("a1",         "b2");    }}

@CsvSource

该注解容许咱们将参数列表作为逗号分隔的值(即 CSV 字符串字面量)传递,每个 CSV 记录都会导致执行一次参数化测试。它还反对应用 useHeadersInDisplayName属性跳过 CSV 标头。

@ParameterizedTest@CsvSource({ "2, even","3, odd"})void checkCsvSource(int number, String expected) {    assertEquals(StringUtils.equals(expected, "even")     ? 0 : 1, number % 2);}

@CsvFileSource

该注解容许咱们应用类门路或本地文件系统中的逗号分隔值(CSV)文件。与 @CsvSource 相似,每个 CSV 记录都会导致执行一次参数化测试。它还反对各种其余属性 -numLinesToSkip、useHeadersInDisplayName、lineSeparator、delimiterString等。

示例 1: 根本实现

@ParameterizedTest@CsvFileSource(files = "src/test/resources/csv-file-source.csv",numLinesToSkip = 1)void checkCsvFileSource(int number, String expected) {    assertEquals(StringUtils.equals(expected, "even")                 ? 0 : 1, number % 2);}

src/test/resources/csv-file-source.csv

NUMBER, ODD_EVEN

2, even

3, odd

示例2:应用属性

@ParameterizedTest@CsvFileSource(    files = "src/test/resources/csv-file-source_attributes.csv",    delimiterString = "|",    lineSeparator = "||",    numLinesToSkip = 1)void checkCsvFileSourceAttributes(int number, String expected) {    assertEquals(StringUtils.equals(expected, "even")? 0 : 1, number % 2);}

src/test/resources/csv-file-source_attributes.csv

|| NUMBER | ODD_EVEN ||

|| 2 | even ||

|| 3 | odd ||

@EnumSource

该注解提供了一种不便的办法来应用枚举常量作为测试用例参数。反对的属性包含:

  • value - 枚举类类型,例如 ChronoUnit.class
package java.time.temporal;public enum ChronoUnit implements TemporalUnit {    SECONDS("Seconds", Duration.ofSeconds(1)),    MINUTES("Minutes", Duration.ofSeconds(60)),HOURS("Hours", Duration.ofSeconds(3600)),    DAYS("Days", Duration.ofSeconds(86400)),    //12 other units}

ChronoUnit 是一个蕴含规范日期周期单位的枚举类型。

@ParameterizedTest@EnumSource(ChronoUnit.class)void checkEnumSourceValue(ChronoUnit unit) {assertNotNull(unit);}

在此示例中,@EnumSource 将传递所有16个 ChronoUnit 枚举值作为参数。

  • names - 枚举常量的名称或抉择名称的正则表达式,例如 DAYS 或 ^.*DAYS$
@ParameterizedTest@EnumSource(names = { "DAYS", "HOURS" })void checkEnumSourceNames(ChronoUnit unit) {    assertNotNull(unit);}

@ArgumentsSource

该注解提供了一个自定义的可重用ArgumentsProvider。ArgumentsProvider的实现必须是外部类或动态嵌套类。

  • 内部参数提供程序
public class ArgumentsSourceTest {    @ParameterizedTest    @ArgumentsSource(ExternalArgumentsProvider.class)    void checkExternalArgumentsSource(int number, String expected) {        assertEquals(StringUtils.equals(expected, "even")                    ? 0 : 1, number % 2,                    "Supplied number " + number +                    " is not an " + expected + " number");    }}public class ExternalArgumentsProvider implements ArgumentsProvider {    @Override    public Stream<? extends Arguments> provideArguments(        ExtensionContext context) throws Exception {        return Stream.of(Arguments.of(2, "even"),             Arguments.of(3, "odd"));    }}
  • 动态嵌套参数提供程序
public class ArgumentsSourceTest {    @ParameterizedTest    @ArgumentsSource(NestedArgumentsProvider.class)    void checkNestedArgumentsSource(int number, String expected) {        assertEquals(StringUtils.equals(expected, "even")? 0 : 1, number % 2,                 "Supplied number " + number +                    " is not an " + expected + " number");    }    static class NestedArgumentsProvider implements ArgumentsProvider {        @Override        public Stream<? extends Arguments> provideArguments(            ExtensionContext context) throws Exception {            return Stream.of(Arguments.of(2, "even"),     Arguments.of(3, "odd"));        }    }}

参数转换

首先,设想一下如果没有参数转换,咱们将不得不本人解决参数数据类型的问题。

源办法: Calculator 类

public int sum(int a, int b) {    return a + b;}

测试用例:

@ParameterizedTest@CsvSource({ "10, 5, 15" })void calculateSum(String num1, String num2, String expected) {    int actual = calculator.sum(Integer.parseInt(num1),                                Integer.parseInt(num2));    assertEquals(Integer.parseInt(expected), actual);}

如果咱们有String参数,而咱们正在测试的源办法承受Integers,则在调用源办法之前,咱们须要负责进行此转换。

JUnit5 提供了不同的参数转换形式

  • 扩大原始类型转换
@ParameterizedTest@ValueSource(ints = { 2, 4 })void checkWideningArgumentConversion(long number) {    assertEquals(0, number % 2);}

应用 @ValueSource(ints = { 1, 2, 3 }) 进行参数化测试时,能够申明承受 int、long、float 或 double 类型的参数。

  • 隐式转换
@ParameterizedTest@ValueSource(strings = "DAYS")void checkImplicitArgumentConversion(ChronoUnit argument) {    assertNotNull(argument.name());}

JUnit5提供了几个内置的隐式类型转换器。转换取决于申明的办法参数类型。例如,用@ValueSource(strings = "DAYS")正文的参数化测试会隐式转换为类型ChronoUnit的参数。

  • 回退字符串到对象的转换
@ParameterizedTest@ValueSource(strings = { "Name1", "Name2" })void checkImplicitFallbackArgumentConversion(Person person) {    assertNotNull(person.getName());}public class Person {    private String name;    public Person(String name) {        this.name = name;    }    //Getters & Setters}

JUnit5提供了一个回退机制,用于主动将字符串转换为给定指标类型,如果指标类型申明了一个实用的工厂办法或工厂构造函数。例如,用@ValueSource(strings = { "Name1", "Name2" })正文的参数化测试能够申明承受一个类型为Person的参数,其中蕴含一个名为name且类型为string的单个字段。

  • 显式转换
@ParameterizedTest@ValueSource(ints = { 100 })void checkExplicitArgumentConversion(    @ConvertWith(StringSimpleArgumentConverter.class) String argument) {    assertEquals("100", argument);}public class StringSimpleArgumentConverter extends SimpleArgumentConverter {    @Override    protected Object convert(Object source, Class<?> targetType)        throws ArgumentConversionException {        return String.valueOf(source);    }}

如果因为某种原因,您不想应用隐式参数转换,则能够应用@ConvertWith正文来定义本人的参数转换器。例如,用@ValueSource(ints = { 100 })正文的参数化测试能够申明承受一个类型为String的参数,应用
StringSimpleArgumentConverter.class将整数转换为字符串类型。

参数聚合

@ArgumentsAccessor

默认状况下,提供给@ParameterizedTest办法的每个参数对应于一个办法参数。因而,当提供大量参数的参数源能够导致大型办法签名时,咱们能够应用ArgumentsAccessor而不是申明多个参数。类型转换反对如下面的隐式转换所述。

@ParameterizedTest@CsvSource({ "John, 20",         "Harry, 30" })void checkArgumentsAccessor(ArgumentsAccessor arguments) {    Person person = new Person(arguments.getString(0),                             arguments.getInteger(1));    assertTrue(person.getAge() > 19, person.getName() + " is a teenager");}

自定义聚合器

咱们看到ArgumentsAccessor能够间接拜访@ParameterizedTest办法的参数。如果咱们想在多个测试中申明雷同的ArgumentsAccessor怎么办?JUnit5通过提供自定义可重用的聚合器来解决此问题。

  • @AggregateWith
@ParameterizedTest@CsvSource({ "John, 20",             "Harry, 30" })void checkArgumentsAggregator(    @AggregateWith(PersonArgumentsAggregator.class) Person person) {    assertTrue(person.getAge() > 19, person.getName() + " is a teenager");}public class PersonArgumentsAggregator implements ArgumentsAggregator {    @Override    public Object aggregateArguments(ArgumentsAccessor arguments,        ParameterContext context) throws ArgumentsAggregationException {        return new Person(arguments.getString(0),arguments.getInteger(1));    }}

实现ArgumentsAggregator接口并通过@AggregateWith正文在@ParameterizedTest办法中注册它。当咱们执行测试时,它会将聚合后果作为对应测试的参数提供。ArgumentsAggregator的实现能够是外部类或动态嵌套类。

额定福利

因为您曾经浏览完文章,我想给您一个额定的福利 - 如果您正在应用像Fluent assertions for java这样的断言框架,则能够将
java.util.function.Consumer作为参数传递,其中蕴含断言自身。

@ParameterizedTest@MethodSource("checkNumberArgs")void checkNumber(int number, Consumer<Integer> consumer) {    consumer.accept(number);    }static Stream<Arguments> checkNumberArgs() {        Consumer<Integer> evenConsumer =            i -> Assertions.assertThat(i % 2).isZero();    Consumer<Integer> oddConsumer =            i -> Assertions.assertThat(i % 2).isEqualTo(1);    return Stream.of(Arguments.of(2, evenConsumer),         Arguments.of(3, oddConsumer));}

总结

JUnit5的参数化测试性能通过打消反复测试用例的须要,提供屡次应用不同输出运行雷同测试的能力,实现了高效的测试。这不仅为开发团队节俭了工夫和精力,而且还减少了测试过程的覆盖范围和有效性。此外,该性能容许对源代码进行更全面的测试,因为能够应用更宽泛的输出进行测试,从而减少了辨认任何潜在的谬误或问题的机会。总体而言,JUnit5的参数化测试是进步代码品质和可靠性的有价值的工具。


【注】本文译自: JUnit 5 Parameterized Tests (reflectoring.io)