[跟着官方文档学JUnit5][六][WritingTests][学习笔记]

举报
John2021 发表于 2022/04/28 22:13:57 2022/04/28
【摘要】 1.重复测试JUnit Jupiter提供了通过注解具有@RepeatedTest并指定所需重复总重复次数的方法来重复指定次数测试的能力。重复测试的每次调用都类似于执行常规@Test方法,完全支持相同的生命周期回调和扩展。下面演示了如何声明一个名为repeatedTest()的测试,该测试将自动重复10次@RepeatedTest(10)void repeatedTest(){ //...

1.重复测试

JUnit Jupiter提供了通过注解具有@RepeatedTest并指定所需重复总重复次数的方法来重复指定次数测试的能力。重复测试的每次调用都类似于执行常规@Test方法,完全支持相同的生命周期回调和扩展。
下面演示了如何声明一个名为repeatedTest()的测试,该测试将自动重复10次

@RepeatedTest(10)
void repeatedTest(){
    //...
}

除了指定重复次数之外,还可以通过@RepeatedTest注解的name属性为每个重复配置自定义显示名称。此外,显示名称可以是静态文本和动态占位符组合的模式。当前支持以下占位符。

  • {displayName}:显示@RepeatedTest方法的名字
  • {currentRepetition}:当前重复计数
  • {totalRepetitions}:重复次数总数

给定重复的默认显示名称是根据以下模式生成的:“{totalRepetitions} 的重复 {currentRepetition}”。因此,上一个重复的Test()示例的单个重复的显示名称将是:repetition 1 of 10,repetition 2 of 10,依此类推。如果您希望@RepeatedTest方法的显示名称包含在每次重复的名称中,则可以定义自己的自定义模式或使用预定义的RepeatedTest.LONG_DISPLAY_NAME模式。后者等于"{displayName} :: repetition {currentRepetition} of {totalRepetitions}",为单独的重复显示名字如"repeatedTest() :: repetition 1 of 10, repeatedTest() :: repetition 2 of 10"。
为了以编程方式检索有关当前重复的重复总数的信息,开发人员可以选择将RepeatInfo的实例注入到@RepeatedTest、@BeforeEach或@AfterEach方法中。

1.1.重复测试例子

repeatedTest()方法与上面的示例相同;repeatedTestWithRepetitionInfo()演示了如何将RepeatInfo的实例注入到测试中,以访问当前重复测试的重复总数。
接下来的两个方法演示如何在每次重复显示名称中@RepeatedTest方法的自定义@DisplayName。customDisplayName()将自定义显示名称与自定义模式结合,然后使用TestInfo验证生成的显示名称的格式。Repeat!是来自@DisplayName声明的{displayName},1/1来自{currentRepetition}/{totalRepetitions}。相比之下,customDisplayNameWithLongPattern()使用预定义RepeatedTest.LONG_DISPLAY_NAME模式。
由于beforeEach()方法使用@BeforeEach因此它将在每次重复测试的每次重复之前执行。通过将TestInfo和RepeatRepetedInfo注入到方法中,我们看到可以获取有关当前正在执行的重复测试的信息。在启用INFO日志级别的情况下执行重复测试演示将产生以下输出。

INFO: About to execute repetition 1 of 10 for repeatedTest
INFO: About to execute repetition 2 of 10 for repeatedTest
INFO: About to execute repetition 3 of 10 for repeatedTest
INFO: About to execute repetition 4 of 10 for repeatedTest
INFO: About to execute repetition 5 of 10 for repeatedTest
INFO: About to execute repetition 6 of 10 for repeatedTest
INFO: About to execute repetition 7 of 10 for repeatedTest
INFO: About to execute repetition 8 of 10 for repeatedTest
INFO: About to execute repetition 9 of 10 for repeatedTest
INFO: About to execute repetition 10 of 10 for repeatedTest

代码示例:

import static org.junit.jupiter.api.Assertions.assertEquals;

import java.util.logging.Logger;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.RepeatedTest;
import org.junit.jupiter.api.RepetitionInfo;
import org.junit.jupiter.api.TestInfo;

class RepeatedTestsDemo {

    private Logger logger=Logger.getLogger("RepeatedTestsDemo");  //根据类名创建Logger

    @BeforeEach
    void beforeEach(TestInfo testInfo, RepetitionInfo repetitionInfo) {
        int currentRepetition = repetitionInfo.getCurrentRepetition();
        int totalRepetitions = repetitionInfo.getTotalRepetitions();
        String methodName = testInfo.getTestMethod().get().getName();
        logger.info(String.format("About to execute repetition %d of %d for %s",currentRepetition,totalRepetitions,methodName));
    }

    @RepeatedTest(10)
    void repeatedTest() {
        // ...
    }

    @RepeatedTest(5)
    void repeatedTestWithRepetitionInfo(RepetitionInfo repetitionInfo) {
        assertEquals(5, repetitionInfo.getTotalRepetitions());
    }

    @RepeatedTest(value = 1, name = "{displayName} {currentRepetition}/{totalRepetitions}")
    @DisplayName("Repeat!")
    void customDisplayName(TestInfo testInfo) {
        assertEquals("Repeat! 1/1", testInfo.getDisplayName());
    }

    @RepeatedTest(value = 1, name = RepeatedTest.LONG_DISPLAY_NAME)
    @DisplayName("Details...")
    void customDisplayNameWithLongPattern(TestInfo testInfo) {
        assertEquals("Details... :: repetition 1 of 1", testInfo.getDisplayName());
    }

    @RepeatedTest(value = 5, name = "Wiederholung {currentRepetition} von {totalRepetitions}")
    void repeatedTestInGerman() {
        // ...
    }

}

输出结果

2.参数化测试(JUnit5重点)

参数化测试使得使用不同参数多次运行测试成为可能。声明方式与@Test方法相同,但改用@ParameterizedTest注解。此外,必须声明至少一个将为每个调用提供参数的源,然后使用测试方法中的参数。
以下示例演示了一个参数化测试,该测试使用@ValueSource注解将字符串数组指定为参数源。

public class StringUtils {
    /*
     * 判断回文字符串
     * */
    public static boolean isPalindrome(String str) {
        if (str == null || str.length() == 0) {
            throw new RuntimeException("Empty String");
        }
        int mid = (str.length() - 1) / 2;
        for (int i = 0; i <= mid; i++) {
            if (str.charAt(i) != str.charAt(str.length() - 1 - i)) {
                return false;
            }
        }
        return true;
    }
}
import com.example.util.StringUtils;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

import static org.junit.jupiter.api.Assertions.*;

class ParameterizedTestsDemo {
    @ParameterizedTest
    @ValueSource(strings = {"racecar", "radar", "able was I ere I saw elba"})
    void palindromes(String candidate) {
        assertTrue(StringUtils.isPalindrome(candidate));
    }
}

输出结果

2.1.要求进行的配置

为了使用参数化测试,需要添加对junit-jupiter-params的依赖关系

2.2.使用参数

参数化测试方法通常直接使用来自以配置源的参数,在参数源索引和方法参数索引之间实现一对一的关联。但是,参数化测试方法也可以选择将源中的参数聚合到传递给该方法的单个对象中。参数解析器也可以提供其他参数(例如,获取TestInfo、TestReporter等实例)。具体而言,参数化测试方法必须根据以下规则声明形式化参数

  • 必须首先声明零个或多个索引参数
  • 接下来必须声明零个或多个聚合器
  • 参数解析程序提供的零个或多个参数必须声明

在此上下文中,索引参数是ArgumentsProvider提供的“参数”中给定索引的参数,该参数作为参数传递给方法的正式参数列表中同一索引处的参数化方法。聚合器是参数访问器类型的任何参数,或者用@AggregateWith注解的任何参数。

2.3.参数的资源

开箱即用,JUnit Jupiter提供了相当多的源注解。

@ValueSource

@ValueSource是最简单的来源之一。它允许您指定单个文本值数组,并且只能用于为每个参数化测试调用提供单个参数。
@ValueSource支持以下类型的文本值。

  • short
  • byte
  • int
  • long
  • float
  • double
  • char
  • boolean
  • java.lang.String
  • java.lang.Class

例如,将调用以下@ParameterizedTest方法三次,值分别为 1、2 和 3。

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

import static org.junit.jupiter.api.Assertions.*;

class ValueSourceDemo {
    @ParameterizedTest
    @ValueSource(ints = {1, 2, 3})
    void testWithValueSource(int argument) {
        assertTrue(argument > 0 && argument < 4);
    }
}

Null and Empty Sources

为了检查极端情况并验证软件在提供错误输入时的正确行为,将null值和空值提供给参数化测试可能很有用。以下注解用作接受单个参数的参数化测试的null值和空值的来源。

  • @NullSource:为带注解的@ParameterizedTest方法提供单个null参数。不能用于具有primitive类型参数
  • @EmptySource:为以下类型的参数为带注解的@ParameterizedTest方法提供单个空参数:java.lang.String,java.util.List,java.util.Set,java.util.Map,基元数组(例如,int[],char[][]等),对象数组(例如,String[],Integer[][]等)。不支持受支持类型的子类型。
  • @NullAndEmptySource:组合注解,结合了@NullSource和@EmptySource的功能。

如果你需要为参数化测试提供多种不同类型的空白字符串,可以使用@ValueSource。例如,@ValueSource(strings = {" ", " ", “\t”, “\n”}) .
还可以结合@NullSource、@EmptySource和@ValueSource来测试更广泛的null、empty和blank输入。以下示例演示了如何为字符串实现此目的。

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.EmptySource;
import org.junit.jupiter.params.provider.NullSource;
import org.junit.jupiter.params.provider.ValueSource;

import static org.junit.jupiter.api.Assertions.*;

class NullAndEmptyDemo {
    @ParameterizedTest
    @NullSource
    @EmptySource
    @ValueSource(strings = {" ", "  ", "\t", "\n"})
    void nullEmptyAndBlankStrings(String text) {
        assertTrue(text == null || text.trim().isEmpty());
    }
}

使用组合的@NullAndEmptySource注解将上述内容简化如下。

@ParameterizedTest
@NullAndEmptySource
@ValueSource(strings = { " ", "   ", "\t", "\n" })
void nullEmptyAndBlankStrings(String text) {
    assertTrue(text == null || text.trim().isEmpty());
}

@EnumSource

@EnumSource提供了一种使用Enum常量的便捷方式。

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.EnumSource;

import java.time.temporal.ChronoUnit;
import java.time.temporal.TemporalUnit;

import static org.junit.jupiter.api.Assertions.*;

class EnumSourceDemo {
    @ParameterizedTest
    @EnumSource(ChronoUnit.class)
    void testWithEnumSource(TemporalUnit unit) {
        assertNotNull(unit);
    }
}

输出结果

注解的value属性是可选的。省略时,使用第一个方法参数的声明类型。如果不引用枚举类型,测试将失败。因此,在上面的示例中需要value属性,因为方法参数声明为TemporalUnit,即由ChronoUnit实现的接口,它不是枚举类型。将方法参数类型更改为ChronoUnit允许从注解中省略显式枚举类型,如下所示。

@ParameterizedTest
@EnumSource
void testWithEnumSourceWithAutoDetection(ChronoUnit unit) {
        assertNotNull(unit);
}

注解提供了一个可选的名称属性,可让您指定应使用哪些常量,如下例所示。如果省略,将使用所有常量。

@ParameterizedTest
@EnumSource(names = {"DAYS", "HOURS"})
void testWithEnumSourceInclude(ChronoUnit unit) {
    assertTrue(EnumSet.of(ChronoUnit.DAYS, ChronoUnit.HOURS).contains(unit));
}

输出结果

@EnumSource注解还提供了一个可选的mode属性,可以对哪些常量传递给测试方法进行细粒度控制。例如,您可以从枚举常量池中排除名称或指定正则表达式,如下例所示。

@ParameterizedTest
@EnumSource(mode = EnumSource.Mode.EXCLUDE, names = {"ERAS", "FOREVER"})
void testWithEnumSourceExclude(ChronoUnit unit) {
    assertFalse(EnumSet.of(ChronoUnit.ERAS, ChronoUnit.FOREVER).contains(unit));
}

@ParameterizedTest
@EnumSource(mode = MATCH_ALL, names = "^.*DAYS$")
void testWithEnumSourceRegex(ChronoUnit unit) {
    assertTrue(unit.name().endsWith("DAYS"));
}

@MethodSource

@MethodSource允许引用测试类或外部类的一个或多个工厂方法。
测试类中的工厂方法必须是静态的,除非测试类使用@TestInstance(Lifecycle.PER_CLASS);注解。而外部类中的工厂方法必须始终是静态的。此外,此类工厂方法不得接受任何参数。
每个工厂方法都必须生成一个参数流,并且流中的每组参数都将作为物理参数提供给带注解的@ParameterizedTest方法的单独调用。一般来说,这会转化为参数流(即Stream<Arguments>);但是,实际的具体返回类型可以采用多种形式。在此上下文中,"流"是JUnit可以可靠地转换为流的任何内容,例如 Stream、DoubleStream、LongStream、IntStream、Collection、Iterator、Iterable、对象数组或基元数组。如果参数化测试方法接受单个参数,则流中的"参数"可以作为参数实例、对象数组(例如 Object[])或单个值提供。
如果您只需要一个参数,则可以返回参数类型实例的Stream,如下例所示。

@ParameterizedTest
@MethodSource("stringProvider")
void testWithExplicitLocalMethodSource(String argument) {
    assertNotNull(argument);
}
static Stream<String> stringProvider() {
    return Stream.of("apple", "banana");
}

如果没有通过@MethodSource显式提供工厂方法名称,JUnit Jupiter将按照约定搜索与当前@ParameterizedTest方法同名的工厂方法。这在以下示例中进行了演示。

@ParameterizedTest
@MethodSource
void testWithDefaultLocalMethodSource(String argument) {
    assertNotNull(argument);
}

static Stream<String> testWithDefaultLocalMethodSource() {
    return Stream.of("apple", "banana");
}

原始类型(DoubleStream、IntStream 和 LongStream)的流也受支持,如下例所示。

@ParameterizedTest
@MethodSource("range")
void testWithRangeMethodSource(int argument) {
    //assertNotEquals(16, argument);//报错
    assertNotEquals(9,argument);
}
static IntStream range() {
    return IntStream.range(0, 20).skip(10);
}

如果参数化测试方法声明了多个参数,则需要返回Arguments实例或对象数组的集合、流或数组,如下所示(有关支持的返回类型的更多详细信息,请参见@MethodSource)。请注意,arguments(Object…​)是在Arguments接口中定义的静态工厂方法。此外,Arguments.of(Object…​)可以用作arguments(Object…​)的替代品。

    @ParameterizedTest
    @MethodSource("stringIntAndListProvider")
    void testWithMultiArgMethodSource(String str, int num, List<String> list) {
        assertEquals(5, str.length());
        assertTrue(num >= 1 && num <= 2);
        assertEquals(2, list.size());
    }
    static Stream<Arguments> stringIntAndListProvider() {
        return Stream.of(
                Arguments.arguments("apple", 1, Arrays.asList("a", "b")),
                Arguments.arguments("lemon", 2, Arrays.asList("x", "y"))
        );
    }

输出结果

可以通过提供其完全限定的方法名称来引用外部静态工厂方法,如下例所示。

package com.example.parameterized;

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;

import java.util.stream.Stream;

class ExternalMethodSourceDemo {
    @ParameterizedTest
    @MethodSource("com.example.parameterized.StringProviders#tinyStrings")
    void testWithExternalMethodSource(String tinyStrings) {
        // test with tiny string
    }
}

class StringProviders {
    static Stream<String> tinyStrings() {
        return Stream.of(".", "oo", "OOO");
    }
}

@CsvSource

@CsvSource允许您将参数列表表示为逗号分隔值(即CSV字符串文字)。通过@CsvSource中的value属性提供的每个字符串都代表一个CSV记录,并导致一次参数化测试的调用。第一条记录可以选择用于提供CSV标头(有关详细信息和示例,请参阅Javadoc中的useHeadersInDisplayName属性)。

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;

import static org.junit.jupiter.api.Assertions.*;

class CsvSourceDemo {
    @ParameterizedTest
    @CsvSource({
            "apple,    1",
            "banana,    2",
            "'lemon, lime',0xF1",
            "strawberry,    700_000"
    })
    void testWithCsvSource(String fruit, int rank) {
        assertNotNull(fruit);
        assertNotEquals(0, rank);
    }
}

默认分隔符是逗号(,),但可以通过设置delimiter属性来使用其他字符。或者,delimiterString属性允许您使用字符串分隔符而不是单个字符。但是,不能同时设置两个分隔符属性。
默认情况下,@CsvSource使用单引号(’)作为其引号字符,但这可以通过quoteCharacter属性进行更改。请参阅上例和下表中的“lemon,lime”值。除非设置了emptyValue属性,否则带引号的空值(’’)会导致空字符串;然而,一个完全空的值被解释为一个空引用。通过指定一个或多个nullValues,可以将自定义值解释为null引用(参见下表中的NIL示例)。如果空引用的目标类型是原始类型,则会引发ArgumentConversionException。
除带引号的字符串外,默认情况下会修改CSV列中的头部和尾部空格。可以通过将ignoreLeadingAndTrailingWhitespace属性设置为 true 来更改此行为。

示例 结果参数列表
@CsvSource({ “apple, banana” }) “apple”, “banana”
@CsvSource({ “apple, ‘lemon, lime’” }) “apple”, “lemon, lime”
@CsvSource({ “apple, ‘’” }) “apple”, “”
@CsvSource({ "apple, " }) “apple”, null
@CsvSource(value = { “apple, banana, NIL” }, nullValues = “NIL”) “apple”, “banana”, null
@CsvSource(value = { " apple , banana" }, ignoreLeadingAndTrailingWhitespace = false) " apple “, " banana”

如果使用的编程语言支持文本块 — 例如Java SE 15或更高版本 — 也可以使用@CsvSource的textBlock属性。文本块中的每条记录代表一个CSV记录,并导致一次参数化测试的调用。通过将useHeadersInDisplayName属性设置为true,可以选择使用第一条记录来提供CSV标头。

@CsvFileSource

@CsvFileSource允许使用来自类路径或本地文件系统的逗号分隔值(CSV)文件。CSV文件中的每条记录都会导致一次参数化测试的调用。第一条记录可以选择用于提供CSV标头。可以通过numLinesToSkip属性指示JUnit忽略标题。如果希望在显示名称中使用标题,可以将useHeadersInDisplayName属性设置为true。下面的示例演示了numLinesToSkip和useHeadersInDisplayName的使用。
默认分隔符是逗号(,),但可以通过设置delimiter属性来使用其他字符。或者,delimiterString属性允许使用字符串分隔符而不是单个字符。但是,不能同时设置两个分隔符属性。
注意:任何以# 符号开头的行都将被解释为注解并被忽略。

two-column.csv
COUNTRY, REFERENCE
Sweden, 1
Poland, 2
"United States of America", 3
France, 700_000
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvFileSource;
import org.junit.jupiter.params.provider.CsvSource;

import static org.junit.jupiter.api.Assertions.*;

class CsvSourceDemo {
    /*
    * Maven resource folder in test folder
    * */
    @ParameterizedTest
    @CsvFileSource(resources = "/two-column.csv", numLinesToSkip = 1)
    void testWithCsvFileSourceFromClasspath(String country, int reference) {
        assertNotNull(country);
        assertNotEquals(0, reference);
    }

    @ParameterizedTest
    @CsvFileSource(resources = "src/main/resources/two-column.csv", numLinesToSkip = 1)
    void testWithCsvFileSourceFromFile(String country, int reference) {
        assertNotNull(country);
        assertNotEquals(0, reference);
    }

    /*
     * Maven resource folder in test folder
     * */
    @ParameterizedTest(name = "[{index}] {arguments}")
    @CsvFileSource(resources = "/two-column.csv", useHeadersInDisplayName = true)
    void testWithCsvFileSourceAndHeaders(String country, int reference) {
        assertNotNull(country);
        assertNotEquals(0, reference);
    }
}

输出结果:
前两个测试用例

最后一个测试用例

ArgumentsSource

@ArgumentsSource可用于指定自定义的、可重用的ArgumentsProvider。必须将ArgumentsProvider的实现声明为顶级类或静态嵌套类。

package com.example.parameterized;

import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.ArgumentsProvider;

import java.util.stream.Stream;

public class MyArgumentsProvider implements ArgumentsProvider {

    @Override
    public Stream<? extends Arguments> provideArguments(ExtensionContext extensionContext) throws Exception {
        return Stream.of("apple", "banana").map(Arguments::of);
    }
}
package com.example.parameterized;

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ArgumentsSource;

import static org.junit.jupiter.api.Assertions.*;

class ArgumentsSourceDemo {
    @ParameterizedTest
    @ArgumentsSource(MyArgumentsProvider.class)
    void testWithArgumentsSource(String argument) {
        assertNotNull(argument);
    }
}

2.4.参数转换

Widening Conversion

JUnit Jupiter支持对提供给@ParameterizedTest的参数进行Widening Primitive Conversion。例如,使用@ValueSource(ints = { 1, 2, 3 })注解的参数化测试不仅可以声明为接受int类型的参数,还可以接受long、float或double类型的参数。

Implicit Conversion

为了支持像@CsvSource这样的用例,JUnit Jupiter提供了许多内置的隐式类型转换器。转换过程取决于每个方法参数的声明类型。
例如,如果 @ParameterizedTest声明了TimeUnit类型的参数,并且声明的源提供的实际类型是String,则该字符串将自动转换为相应的TimeUnit枚举常量。

@ParameterizedTest
@ValueSource(strings = "SECONDS")
void testWithImplicitArgumentConversion(ChronoUnit argument) {
    assertNotNull(argument.name());
}

输出结果

字符串实例被隐式转换为以下目标类型。

目标类型 示例
boolean/Boolean “true”->true
byte/Byte “15”,“0xF”,“017”->(byte)15
char/Character “o”->‘o’
short/Short “15”,“0xF”,“017”->(short)15
int/Integer “15”,“0xF”,“017”->15
long/Long “15”,“0xF”,“017”->15L
float/Float “1.0”->1.0f
double/Double “1.0”->1.0d
Enum subclass “SECONDS”->TimeUnit.SECONDS
java.io.File “/path/to/file”->new File("/path/to/file")
java.lang.Class “java.lang.Integer”->java.lang.Integer.class (use $ for nested classes, e.g. “java.lang.Thread$State”)
java.lang.Class “byte”->byte.class(primitive types are supported)
java.lang.Class “char[]”->char[].class (array types are supported)
java.math.BigDecimal “123.456e789”->new BigDecimal(“123.456e789”)
java.math.BigInteger “1234567890123456789”->new BigInteger(“1234567890123456789”)
java.net.URI https://junit.org/"->URI.create("https://junit.org/”)
java.net.URL https://junit.org/”->new URL(“https://junit.org/”)
java.nio.charset.Charset “UTF-8”->Charset.forName(“UTF-8”)
java.nio.file.Path “/path/to/file”->Paths.get("/path/to/file")
java.time.Duration “PT3S”->Duration.ofSeconds(3)
java.time.Instant “1970-01-01T00:00:00Z”->Instant.ofEpochMilli(0)
java.time.LocalDateTime “2017-03-14T12:34:56.789”->LocalDateTime.of(2017, 3, 14, 12, 34, 56, 789_000_000)
java.time.LocalDate “2017-03-14”->LocalDate.of(2017, 3, 14)
java.time.LocalTime “12:34:56.789”->LocalTime.of(12, 34, 56, 789_000_000)
java.time.MonthDay “–03-14”->MonthDay.of(3, 14)
java.time.OffsetDateTime “2017-03-14T12:34:56.789Z”->OffsetDateTime.of(2017, 3, 14, 12, 34, 56, 789_000_000, ZoneOffset.UTC)
java.time.OffsetTime “12:34:56.789Z”->OffsetTime.of(12, 34, 56, 789_000_000, ZoneOffset.UTC)
java.time.Period “P2M6D”->Period.of(0, 2, 6)
java.time.YearMonth “2017-03”->YearMonth.of(2017, 3)
java.time.Year “2017”->Year.of(2017)
java.time.ZonedDateTime “2017-03-14T12:34:56.789Z”->ZonedDateTime.of(2017, 3, 14, 12, 34, 56, 789_000_000, ZoneOffset.UTC)
java.time.ZoneId “Europe/Berlin”->ZoneId.of(“Europe/Berlin”)
java.time.ZoneOffset “+02:30”->ZoneOffset.ofHoursMinutes(2, 30)
java.util.Currency “JPY”->Currency.getInstance(“JPY”)
java.util.Locale “en”->new Locale(“en”)
java.util.UUID “d043e930-7b3b-48e3-bdbe-5a3ccfb833db”->UUID.fromString(“d043e930-7b3b-48e3-bdbe-5a3ccfb833db”)

Fallback String-to-Object Conversion

除了从字符串到上表中列出的目标类型的隐式转换之外,如果目标类型恰好声明了一个合适的工厂方法或工厂构造函数,JUnit Jupiter还提供了一种从String到给定目标类型的自动转换的回退机制,定义如下。

  • 工厂方法:在目标类型中声明的非私有静态方法,它接受单个字符串参数并返回目标类型的实例。方法的名称可以是任意的,不需要遵循任何特定的约定。
  • 工厂构造函数:目标类型中接受单个字符串参数的非私有构造函数。目标类型必须声明为顶级类或静态嵌套类。

注意:如果发现多个工厂方法,它们将被忽略。如果发现工厂方法和工厂构造函数,将使用工厂方法而不是构造函数。
例如,在下面的@ParameterizedTest方法中,Book参数将通过调用Book.fromTitle(String)工厂方法并传递“42 Cats”作为书名。

package com.example.parameterized;

public class Book {
    private final String title;

    private Book(String title) {
        this.title = title;
    }

    public static Book fromTitle(String title) {
        return new Book(title);
    }

    public String getTitle() {
        return this.title;
    }
}
package com.example.parameterized;

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import static org.junit.jupiter.api.Assertions.*;

public class FallbackArgumentConversionDemo {
    @ParameterizedTest
    @ValueSource(strings = "42 Cats")
    void testWithImplicitFallbackArgumentConversion(Book book) {
        assertEquals("42 Cats",book.getTitle());
    }
}

Explicit Conversion

可以使用@ConvertWith注解显式指定ArgumentConverter用于某个参数,而不是依赖于隐式参数转换,如下例所示。必须将ArgumentConverter的实现声明为顶级类或静态嵌套类。

package com.example.parameterized;

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.converter.ConvertWith;
import org.junit.jupiter.params.provider.EnumSource;

import static org.junit.jupiter.api.Assertions.*;

import java.time.temporal.ChronoUnit;

class ExplicitArgumentConversion {
    @ParameterizedTest
    @EnumSource(ChronoUnit.class)
    void testWithExplicitArgumentConversion(
            @ConvertWith(ToStringArgumentConverter.class) String argument
    ) {
        assertNotNull(ChronoUnit.valueOf(argument));
    }
}
package com.example.parameterized;

import org.junit.jupiter.params.converter.ArgumentConversionException;
import org.junit.jupiter.params.converter.SimpleArgumentConverter;

import static org.junit.jupiter.api.Assertions.*;

public class ToStringArgumentConverter extends SimpleArgumentConverter {
    @Override
    protected Object convert(Object o, Class<?> aClass) throws ArgumentConversionException {
        assertEquals(String.class, aClass, "Can only convert to String");
        if (o instanceof Enum<?>) {
            return ((Enum<?>) o).name();
        }
        return String.valueOf(o);
    }
}

如果转换器仅用于将一种类型转换为另一种类型,则可以扩展TypedArgumentConverter以避免样板类型检查。

public class ToLengthArgumentConverter extends TypedArgumentConverter<String, Integer> {
    protected ToLengthArgumentConverter() {
        super(String.class, Integer.class);
    }

    @Override
    protected Integer convert(String source) {
        return (source != null ? source.length() : 0);
    }
}

显式参数转换器旨在由测试和扩展作者实现。因此,junit-jupiter-params只提供一个显式参数转换器,也可以作为参考实现:JavaTimeArgumentConverter。通过组合注解JavaTimeConversionPattern使用。

package com.example.parameterized;

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.converter.JavaTimeConversionPattern;
import org.junit.jupiter.params.provider.ValueSource;
import static org.junit.jupiter.api.Assertions.*;
import java.time.LocalDate;

class ExplicitJavaTimeConverter {
    @ParameterizedTest
    @ValueSource(strings = {"01.01.2017", "31.12.2017"})
    void testWithExplicitJavaTimeConverter(
            @JavaTimeConversionPattern("dd.MM.yyyy")LocalDate argument){
        assertEquals(2017,argument.getYear());
    }
}

2.5.参数聚合

默认情况下,提供给@ParameterizedTest方法的每个参数都对应一个方法参数。 因此,预期会提供大量参数的参数源可能会导致较大的方法签名。
在这种情况下,可以使用ArgumentsAccessor代替多个参数。使用此API,可以通过传递给测试方法的单个参数访问提供的参数。此外,支持类型转换,如隐式转换中所述。

import java.time.LocalDate;

public class Person {
    private String firstName;
    private String lastName;
    private Gender gender;
    private LocalDate dateOfBirth;

    public Person(String firstName, String lastName, Gender gender, LocalDate dateOfBirth) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.gender = gender;
        this.dateOfBirth = dateOfBirth;
    }

    public String getFirstName() {
        return firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public Gender getGender() {
        return gender;
    }

    public LocalDate getDateOfBirth() {
        return dateOfBirth;
    }

}
public enum Gender {
    F, M
}
@ParameterizedTest
@CsvSource({
    "Jane, Doe, F, 1990-05-20",
    "John, Doe, M, 1990-10-22"
})
void testWithArgumentsAccessor(ArgumentsAccessor arguments) {
    Person person = new Person(arguments.getString(0),
                               arguments.getString(1),
                               arguments.get(2, Gender.class),
                               arguments.get(3, LocalDate.class));

    if (person.getFirstName().equals("Jane")) {
        assertEquals(Gender.F, person.getGender());
    }
    else {
        assertEquals(Gender.M, person.getGender());
    }
    assertEquals("Doe", person.getLastName());
    assertEquals(1990, person.getDateOfBirth().getYear());
}

ArgumentsAccessor的实例会自动注入到任何ArgumentsAccessor类型的参数中。

自定义聚合器

除了使用ArgumentsAccessor直接访问@ParameterizedTest方法的参数外,JUnit Jupiter还支持使用自定义的、可重用的聚合器。
要使用自定义聚合器,请实现ArgumentsAggregator接口并通过@ParameterizedTest方法中兼容参数上的@AggregateWith注解注册它。当调用参数化测试时,聚合的结果将作为相应参数的参数提供。注意,必须将ArgumentsAggregator的实现声明为顶级类或静态嵌套类。

import org.junit.jupiter.api.extension.ParameterContext;
import org.junit.jupiter.params.aggregator.ArgumentsAccessor;
import org.junit.jupiter.params.aggregator.ArgumentsAggregationException;
import org.junit.jupiter.params.aggregator.ArgumentsAggregator;

import java.time.LocalDate;

public class PersonAggregator implements ArgumentsAggregator {
    @Override
    public Object aggregateArguments(ArgumentsAccessor arguments, ParameterContext context) throws ArgumentsAggregationException {
        return new Person(
                arguments.getString(0),
                arguments.getString(1),
                arguments.get(2, Gender.class),
                arguments.get(3, LocalDate.class)
        );
    }
}
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.aggregator.AggregateWith;
import org.junit.jupiter.params.provider.CsvSource;

class ArgumentsAggregator {
    @ParameterizedTest
    @CsvSource({
            "Jane, Doe, F, 1990-05-20",
            "John, Doe, M, 1990-10-22"
    })
    void testWithArgumentsAggregator(@AggregateWith(PersonAggregator.class) Person person) {

    }
}

如果发现自己在代码库中为多个参数化测试方法反复声明@AggregateWith(MyTypeAggregator.class),可能希望创建一个自定义组合注解,例如使用@AggregateWith(MyTypeAggregator.class)进行元注解的@CsvToMyType。 以下示例使用自定义@CsvToPerson注解演示了这一点。

@ParameterizedTest
@CsvSource({
    "Jane, Doe, F, 1990-05-20",
    "John, Doe, M, 1990-10-22"
})
void testWithCustomAggregatorAnnotation(@CsvToPerson Person person) {
    // perform assertions against person
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
@AggregateWith(PersonAggregator.class)
public @interface CsvToPerson {
}

2.6.自定义显示名称

默认情况下,参数化测试调用的显示名称包含调用索引和该特定调用的所有参数的字符串表示形式。如果存在于字节码中(对于 Java,必须使用-parameters编译器标志编译测试代码),它们中的每一个前面都有参数名称(除非该参数只能通过ArgumentsAccessor或ArgumentAggregator获得)。
但是可以通过@ParameterizedTest注解的name属性自定义调用显示名称,如下例所示。

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;

class CustomDisplayNames {
    @DisplayName("Display name of container")
    @ParameterizedTest(name = "{index} ==> the rank of ''{0}'' is {1}")
    @CsvSource({"apple,1", "banana,2", "'lemon,lime',3"})
    void testWithCustomDisplayNames(String fruit, int rank) {
        
    }
}

输出结果

自定义显示名称中支持以下占位符。

占位符 描述
{displayName} 显示方法名字
{index} 当前调用索引(基于1)
{arguments} 完整的,逗号分隔的参数列表
{argumentsWithNames} 带有参数名称的完整的、逗号分隔的参数列表
{0},{1},… 独立的引用

使用@MethodSource或@ArgumentSource时,可以为参数命名。如果参数包含在调用显示名称中,将使用此名称,如下例所示。

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Named;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;

import java.io.File;
import java.util.stream.Stream;

class NamedArgumentsDemo {
    @DisplayName("A parameterized test with named arguments")
    @ParameterizedTest(name = "{index}: {0}")
    @MethodSource("namedArguments")
    void testWithNamedArguments(File file) {

    }

    static Stream<Arguments> namedArguments() {
        return Stream.of(Arguments.arguments(Named.of("An important file", new File("path1"))),
                Arguments.arguments(Named.of("Another file", new File("path2"))));
    }
}

输出结果

如果想为项目中的所有参数化测试设置默认名称模式,可以将以下配置添加到junit-platform.properties

junit.jupiter.params.displayname.default = {index}

参数化方法的显示名称根据以下优先规则确定:

  • @ParameterizedTest
  • junit.jupiter.params.displayname.default的值
  • @ParameterizedTest中定义的DEFAULT_DISPLAY_NAME常量

2.7.生命周期和互操作性

参数化测试的每次调用都具有与常规@Test方法相同的生命周期。例如,@BeforeEach方法将在每次调用之前执行。与动态测试类似,调用将出现在IDE的测试树中。可以在同一个测试类中随意混合常规的@Test方法和@ParameterizedTest方法。
可以将ParameterResolver扩展与@ParameterizedTest方法一起使用。但是,由参数源解析的方法参数需要在参数列表中排在第一位。由于测试类可能包含常规测试以及具有不同参数列表的参数化测试,因此不会为生命周期方法(例如 @BeforeEach)和测试类构造函数解析来自参数源的值。

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.TestInfo;
import org.junit.jupiter.api.TestReporter;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

class LifecycleDemo {
    @BeforeEach
    void beforeEach(TestInfo testInfo) {
        //...
    }

    @ParameterizedTest
    @ValueSource(strings = "apple")
    void testWithRegularParameterResolver(String argument, TestReporter testReporter) {
        testReporter.publishEntry("argument", argument);
    }

    @AfterEach
    void afterEach(TestInfo testInfo) {
        // ...
    }
}

输出结果

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。