Skip to content

进阶用法

别名

@DisplayName注解

测试类和测试方法可以使用@DisplayName声明自定义显示名称 - 使用空格,特殊字符,甚至emojis表情符号 - 将由测试runner和测试报告显示。

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.DisplayName;

public class DisplayNameTests {

    @DisplayName("这是用例别名")
    @Test
    void displayName(){
        System.out.println("第一条测试用例正在执行");
    }
}

顺序

@Order注解

默认顺序 JUnit5默认按测试用例首字母排序执行

指定顺序 可以按@Order注解中设定的大小顺序执行

  1. 在测试类上加注解 @TestMethodOrder(MethodOrderer.OrderAnnotation.class)
  2. 在测试方法上前添加@Order(n)

其中,n为数字,按小到大的顺序执行

随机顺序 即每次执行的顺序都是随机的。

操作方法:在calss前加注解 @TestMethodOrder(MethodOrderer.Random.class)

标签

@Tag注解

测试类和测试方法都可以打标签。这些标签以后可以用来过滤测试发现和测试执行。

标签来自org.junit.jupiter.api.Tag类。

标签命名:

  • 标签不能为null或空白。
  • 裁剪标签不能包含空格(“裁剪”意思是前后的空白字符已被删除)
  • 裁剪标签不得包含ISO控制字符。
  • 裁剪标签不得包含以下任何保留字符:,()&|!
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;

@Tag("这是测试类的标签")
@Tag("clifford")
public class TagTests {

    @Test
    @Tag("这是测试方法的标签")
    void tagTest(){
    }
}

假设

JUnit5 Jupiter附带了JUnit4提供的一些assumption方法的子集,并增加了一些适合与Java 8 lambda一起使用的方法。

所有的JUnit Jupiter assumption都是org.junit.jupiter.api.Asumptions类中的静态方法。

import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assumptions.assumeTrue;
import static org.junit.jupiter.api.Assumptions.assumingThat;
import static org.junit.jupiter.api.Assertions.assertEquals;

public class AssumptionTests {

    @Test
    void assumeTrueTest(){
        assumeTrue("CI".equals(System.getenv("ENV")));
        System.out.println("CI环境测试");
    }

    @Test
    void assumingThatTest(){

        assumingThat("DEV".equals(System.getenv("ENV")),
                () -> {
                    System.out.println("此断言只会在DEV环境执行");
                    assertEquals(2, 1+1);
                });

        System.out.println("此断言在所有环境都会执行");
        assertEquals(3, 1+2);
    }
}

禁用

@Disabled注解

使用@Disabled标记的测试方法,被标记的方法会被跳过。

import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;

public class DisabledTests {

    @Test
    @Disabled("演示跳过此用例")
    void willNotRun(){
        System.out.println("不会执行");
    }

    @Test
    void willRun(){
        System.out.println("会正常执行");
    }
}

嵌套

@Nested 注解 嵌套测试给测试编写者更多的能力,来表达几组测试之间的关系。通常用来表达结构层次。

示例

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.EmptyStackException;
import java.util.Stack;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;

@DisplayName("一个栈")
class NestedTests {

    Stack<Object> stack;

    @Test
    @DisplayName("测试使用new实例化")
    void isInstantiatedWithNew() {
        new Stack<>();
    }

    @Nested
    @DisplayName("当使用new关键字")
    class WhenNew {

        @BeforeEach
        @DisplayName("前置处理:实例化对象")
        void createNewStack() {
            stack = new Stack<>();
        }

        @Test
        @DisplayName("测试isEmpty()")
        void isEmpty() {
            assertTrue(stack.isEmpty());
        }

        @Test
        @DisplayName("测试pop()")
        void throwsExceptionWhenPopped() {
            assertThrows(EmptyStackException.class, () -> stack.pop());
        }

        @Test
        @DisplayName("测试peek()")
        void throwsExceptionWhenPeeked() {
            assertThrows(EmptyStackException.class, () -> stack.peek());
        }

        @Nested
        @DisplayName("当入栈一个元素后")
        class AfterPushing {
            String anElement = "an element";

            @BeforeEach
            @DisplayName("前置处理:入栈")
            void pushAnElement() {
                stack.push(anElement);
            }

            @Test
            @DisplayName("测试isEmpty()")
            void isNotEmpty() {
                assertFalse(stack.isEmpty());
            }

            @Test
            @DisplayName("测试pop()")
            void returnElementWhenPopped() {
                assertEquals(anElement, stack.pop());
                assertTrue(stack.isEmpty());
            }

            @Test
            @DisplayName("测试peek()")
            void returnElementWhenPeeked() {
                assertEquals(anElement, stack.peek());
                assertFalse(stack.isEmpty());
            }
        }
    }
}

运行后界面效果:

✔ Test Results 
└─ ✔ 一个栈
    ├─ ✔ 测试使用new实例化
    └─ ✔ 当使用new关键字
        ├─ ✔ 测试peak()
        ├─ ✔ 测试pop()
        ├─ ✔ 测试isEmpty()
        └─ ✔ 当入栈一个元素后
            ├─ ✔ 测试peek()
            ├─ ✔ 测试pop()
            └─ ✔ 测试isEmpty()

重复

@RepeatedTest注解

JUnit Jupiter通过使用@RepeatedTest注解方法并指定所需的重复次数,提供了重复测试指定次数的功能。每次重复测试的调用都像执行常规的@Test方法一样,完全支持相同的生命周期回调和扩展。

以下示例演示了如何声明名为RepeatedTest()的测试,该测试将自动重复3次。

import org.junit.jupiter.api.RepeatedTest;

public class RepeatTests {

    @RepeatedTest(3)
    void repeatedTest(){
        System.out.println("重复");
    }
}

执行界面效果:

✔ repeatedTest() 
├─ ✔ repetition 1 of 3
├─ ✔ repetition 2 of 3
└─ ✔ repetition 3 of 3

除了指定重复次数外,还可以通过@RepeatedTest注解的name属性为每次重复配置自定义显示名称。

此外,显示名称可以是模式,由静态文本和动态占位符的组合而成。

目前支持以下占位符:

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

参数化

@ParameterizedTest注解

参数化测试可以用不同的参数多次运行测试。它们和普通的@Test方法一样声明,但是使用@ParameterizedTest注解。另外,您必须声明至少一个将为每次调用提供参数的来源(source)。

为了使用参数化测试,您需要添加对junit-jupiter-params构建的依赖。

<!--  参数化  -->
<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-params</artifactId>
    <version>5.5.2</version>
    <scope>test</scope>
</dependency>

参数来源

参数化数据来源支持六种注解:

  • @ValueSource
  • @EnumSource
  • @MethodSource
  • @CsvSource
  • @CsvFileSource
  • @ArgumentsSource

**@ValueSource** 它是最简单的来源之一。它可以让你指定一个原生类型(String,int,long或double)的数组,并且只能为每次调用提供一个参数。

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

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

public class ParamValueSourceTests {

    @ParameterizedTest
    @ValueSource(strings = {"java", "jdk"})
    void parameterizedStringTest(String element){
        // 参数化
        assertNotNull(element);
    }

    @ParameterizedTest
    @ValueSource(ints = {3, 4, 5})
    void parameterizedIntTest(int value){
        // 参数化
        assertNotNull(value);
    }

}

**@EnumSource**

该注解提供了一个使用Enum常量的简便方法。该注解提供了一个可选的name参数,可以指定使用哪些常量。如果省略,所有的常量将被使用。

该注解还提供了一个可选的mode参数,可以对将哪些常量传递给测试方法进行细化控制。例如,您可以从枚举常量池中排除名称或指定正则表达式。

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

import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;

import static org.junit.jupiter.params.provider.EnumSource.Mode.EXCLUDE;
import static org.junit.jupiter.params.provider.EnumSource.Mode.MATCH_ALL;

import java.util.EnumSet;
import java.util.concurrent.TimeUnit;

public class ParamEnumSourceTests {

    @ParameterizedTest
    @EnumSource(TimeUnit.class)
    void enumSourceOnly(TimeUnit tu) {
        // 不带任何参数
        assertNotNull(tu);
    }

    @ParameterizedTest
    @EnumSource(value = TimeUnit.class, names = {"DAYS", "HOURS"})
    void enumSourceWithName(TimeUnit tu) {
        // 指定 name 参数设置常量池
        assertTrue(EnumSet.of(TimeUnit.DAYS, TimeUnit.HOURS).contains(tu));
    }

    @ParameterizedTest
    @EnumSource(value = TimeUnit.class, names = {"DAYS", "HOURS"}, mode = EXCLUDE)
    void enumSourceWithMode(TimeUnit tu) {
        // 指定 mode 参数排除常量
        assertFalse(EnumSet.of(TimeUnit.DAYS, TimeUnit.HOURS).contains(tu));
        assertTrue(tu.name().length() > 5);
    }

    @ParameterizedTest
    @EnumSource(value = TimeUnit.class, names = "^(M|N).+SECONDS$", mode = MATCH_ALL)
    void enumSourceWithModeExpr(TimeUnit tu) {
        // 指定 mode 参数,结合正则匹配过滤常量
        String name = tu.name();
        assertTrue(name.startsWith("M") || name.startsWith("N"));
        assertTrue(name.endsWith("SECONDS"));
    }

}

**@MethodSource**

该注解允许你引用一个或多个测试类的工厂方法。这样的方法必须返回一个Stream,Iterable,Iterator或者参数数组。

另外,这种方法不能接受任何参数。默认情况下,除非测试类用@TestInstance(Lifecycle.PER_CLASS)注解,否则这些方法必须是静态的。

  1. 测试用例单参数

如果只需要一个参数,则可以返回参数类型的实例Stream。

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

import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;

import java.util.stream.IntStream;
import java.util.stream.Stream;


public class ParamMethodSourceTests {

    // 工厂方法:参数类型的流
    static Stream<String> strProvider(){
        return Stream.of("Java", "Python", "Go");
    }

    // 工厂方法:原始类型的流
    static IntStream intProvider(){
        return IntStream.range(0, 20).skip(10);
    }

    // 测试用例
    @ParameterizedTest
    @MethodSource("strProvider")
    void methodSourceOne(String value){
        // 数据源来自方法
        assertNotNull(value);
    }

    // 测试用例
    @ParameterizedTest
    @MethodSource("intProvider")
    void methodSourceTwo(int num){
        // 数据源来自方法
        assertNotEquals(1, num);
    }
}

  1. 测试用例多参数

如果测试方法声明多个参数,则需要返回一个集合或Arguments实例流。请注意,Arguments.of(Object…)是Arguments接口中定义的静态工厂方法。

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

import java.util.Arrays;
import java.util.List;
import java.util.stream.IntStream;
import java.util.stream.Stream;

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


public class ParamMethodSourceMultiTests {

    // 工厂方法:提供两组数据
    static Stream<Arguments> multiProvider(){
        return Stream.of(
                Arguments.of("foo", 2, Arrays.asList("a", "b")),
                Arguments.of("bar", 2, Arrays.asList("x", "y"))
        );
    }

    // 测试用例
    @ParameterizedTest
    @MethodSource("multiProvider")
    void methodSourceMulti(String s, int n, List<String> li){
        // 多参数数据源来自方法
        assertEquals(3, s.length());
        assertEquals(2, n);
        assertEquals(2, li.size());
    }
}

**@CsvSource** 该注解允许您将参数列表表示为以英文逗号(,)分隔的值(例如,字符串文字)。

如果数据文本含有逗号,则可以使用'作为转义字符。

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;


public class ParamCsvSourceTests {

    @ParameterizedTest
    @CsvSource({"foo,1", "bar, 2", "'baz, quz', 3"})
    void csvSourceTest(String s, int n){
        // csv数据源
        assertNotNull(s);
        assertNotEquals(0, n);
    }
}

一个空的引用值"''"会得到一个空的字符串;而一个被逗号分割后是完全空的值被解释为一个null引用。

如果null引用的目标类型是基本类型,则引发ArgumentConversionException。

csv文本解析示例:

输入 结果字符列表
@CsvSource({ "foo, bar" }) "foo","bar"
@CsvSource({ "foo, 'baz, qux' " }) "foo","baz, qux"
@CsvSource({ "foo, '' " }) "foo", ""
@CsvSource({ "foo, " }) "foo",null

**@CsvFileSource** 该注解让你可以使用classpath中的CSV文件。CSV文件中的每一行都会成为参数化测试的一次调用。

@CsvSource中使用的转义语法不同,@CsvFileSource注解使用双引号"作为转义字符。例如csv文件中的"Hello, world"值,解析后Hello, world将作为一个整体字符串而不用担心会被分割。

一个空的转义值""会产生一个空字符串;而一个被逗号分隔后完全为空的值被解释为null引用。

如果null引用的目标类型是基本类型,则引发ArgumentConversionException。

在/src/test/resources目录中,创建名为"data.csv"的csv文件,内容如下:

Java, 1
Python, 2
"Hello, world", 3
编写测试用例:
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvFileSource;

import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNotEquals;


public class ParamCsvFileSource {

    @ParameterizedTest
    @CsvFileSource(resources = "data.csv")
    void csvFileSourceTest(String s, int n){
        // 数据源来自csv文件
        assertNotNull(s);
        assertNotEquals(0, n);
    }
}

**@ArgumentsSource** 可以使用该注解指定一个自定义的,可重用的ArgumentsProvider。

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

import java.util.stream.Stream;

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


public class ParamArgumentsSource {

    // 自定义的参数提供者
    static class MyArgProvider implements ArgumentsProvider {

        @Override
        public Stream<? extends Arguments> provideArguments(ExtensionContext extensionContext) {
            return Stream.of("Java", "Go").map(Arguments::of);
        }
    }

    // 测试用例
    @ParameterizedTest
    @ArgumentsSource(MyArgProvider.class)
    void testArgumentsSource(String s){
        assertNotNull(s);
    }
}

参数转换

隐式转换

为了支持像@CsvSource这样的情况,JUnit Jupiter提供了一些内置的隐式类型转换器。 转换过程取决于每个方法参数的声明类型。

例如,如果@ParameterizedTest声明TimeUnit类型的参数,并且声明的源提供的实际类型是String,则该字符串将自动转换为相应的TimeUnit枚举常量。

@ParameterizedTest
@ValueSource(strings = "SECONDS")
void testWithImplicitArgumentConversion(TimeUnit argument) {
    assertNotNull(argument.name());
}
String实例目前隐式转换为以下目标类型。

目标类型 示例
boolean/Boolean "true" → true
byte/Byte "1" → (byte) 1
char/Character "o" → 'o'
short/Short "1" → (short) 1
int/Integer "1" → 1
long/Long "1" → 1L
float/Float "1.0" → 1.0f
double/Double "1.0" → 1.0d
Enum subclass "SECONDS" → TimeUnit.SECONDS
java.time.Instant "1970-01-01T00:00:00Z" → Instant.ofEpochMilli(0)
java.time.LocalDate "2017-03-14" → LocalDate.of(2017, 3, 14)
java.time.LocalDateTime "2017-03-14T12:34:56.789" → LocalDateTime.of(2017, 3, 14, 12, 34, 56, 789_000_000)
java.time.LocalTime "12:34:56.789" → LocalTime.of(12, 34, 56, 789_000_000)
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.Year "2017" → Year.of(2017)
java.time.YearMonth "2017-03" → YearMonth.of(2017, 3)
java.time.ZonedDateTime "2017-03-14T12:34:56.789Z" → ZonedDateTime.of(2017, 3, 14, 12, 34, 56, 789_000_000, ZoneOffset.UTC)

显示转换 可以使用@ConvertWith注解来显式指定ArgumentConverter来用于某个参数,而不是使用隐式参数转换。

@ParameterizedTest
@EnumSource(TimeUnit.class)
void testWithExplicitArgumentConversion(@ConvertWith(ToStringArgumentConverter.class) String argument) {
    assertNotNull(TimeUnit.valueOf(argument));
}
static class ToStringArgumentConverter extends SimpleArgumentConverter {
    @Override
    protected Object convert(Object source, Class< ?> targetType) {
        assertEquals(String.class, targetType, "Can only convert to String");
        return String.valueOf(source);
    }
}

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

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

显示名称

默认情况下,参数化测试调用的显示名称包含该特定调用的所有参数的调用索引和字符串表示。 但是,可以通过@ParameterizedTest注解的name属性自定义调用显示名称。

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

import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;

public class ParamCaseNames {

    @DisplayName("参数化用例自定义名称")
    @ParameterizedTest(name = "{index} ==> first=''{0}'', second={1}")
    @CsvSource({"foo,1", "bar, 2", "'baz, quz', 3"})
    void testCaseNames(String s, int n){
        assertNotNull(s);
        assertNotEquals(0, n);
    }
}
执行后面板中显示:
✔ 参数化用例自定义名称 
├─ ✔ 1 ==> first='Java', second=1
├─ ✔ 2 ==> first='Go', second=2
└─ ✔ 3 ==> first='Hello, world', second=3

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

占位符 描述
{index} 当前调用下标(从1开始)
{arguments} 完成的,逗号分隔的参数列表
{0}, {1}, … 单个参数

动态测试

@TestFactory 注解

JUnit5 Jupiter中的标准@Test注解描述了实现测试用例的方法。这些测试用例是静态的,因为它们是在编译时完全指定的,而且它们的行为不能由运行时发生的任何事情来改变。

@Assumption提供了一种基本的动态行为形式,但是刻意在表达方面受到限制。

除了这些标准测试外,JUnit Jupiter还引入了一种全新的测试编程模型。这种新的测试是动态测试,它是由@TestFactory注解的工厂方法在运行时生成的。

@Test方法相比,@TestFactory方法本身不是测试用例,而是测试用例的工厂。因此,动态测试是工厂的产物。从技术上讲,@TestFactory方法必须返回DynamicNode实例的Stream,Collection,Iterable或Iterator。 DynamicNode的可实例化的子类是DynamicContainerDynamicTest

DynamicContainer 该实例由一个显示名称和一个动态子节点列表组成,可以创建任意嵌套的动态节点层次结构。然后,DynamicTest实例将被延迟执行,从而实现测试用例的动态甚至非确定性生成。 任何由@TestFactory返回的Stream都要通过调用stream.close()来正确关闭,使得使用诸如Files.lines()之类的资源变得安全。 与标准测试@Test方法一样,动态测试@TestFactory方法不能是privatestatic,并且可以选择声明参数,以便通过ParameterResolvers解析。

DynamicTest 是运行时生成的测试用例。它由显示名称和Executable组成。 Executable@FunctionalInterface,这意味着动态测试的实现可以作为lambda表达式或方法引用来提供。

从JUnit Jupiter 5.0.2开始,动态测试必须始终由工厂方法创建。

示例1

import org.junit.jupiter.api.TestFactory;

import java.util.Arrays;
import java.util.List;

public class DynamicTestOne {

    @TestFactory
    List<String> dynamicInvalidReturnType(){
        // 会造成JUitException异常
        return Arrays.asList("Hello");
    }
}
解释:由于在编译时无法检测到无效的返回类型,因此在运行时检测并抛出JUnitException异常。

执行用例后会输出报错信息:

org.junit.platform.commons.JUnitException: @TestFactory method [java.util.List DynamicTestFactory.dynamicInvalidReturnType()] must return a single org.junit.jupiter.api.DynamicNode or a Stream, Collection, Iterable, Iterator, or array of org.junit.jupiter.api.DynamicNode.

示例2

import org.junit.jupiter.api.DynamicTest;
import org.junit.jupiter.api.TestFactory;
import java.util.Arrays;
import java.util.Collection;
import java.util.stream.IntStream;
import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.DynamicTest.dynamicTest;

public class DynamicTestTwo {

    @TestFactory
    Collection<DynamicTest> fromCollection(){
        return Arrays.asList(
                dynamicTest("1st dynamic test", () -> assertTrue(true))
        );
    }

    @TestFactory
    Iterable<DynamicTest> fromIterable(){
        return Arrays.asList(
                dynamicTest("2nd dynamic test", () -> assertEquals(2, 1+1))
        );
    }

    @TestFactory
    Stream<DynamicTest> fromStream(){
        return Stream
                .of("A", "B", "C")
                .map(str -> dynamicTest("test" + str, () -> { /* ... */}));
    }

    @TestFactory
    Stream<DynamicTest> fromIntStream(){
        // 给从0开始的前10个偶数生成测试用例
        return IntStream
                .iterate(0, n -> n+2)
                .limit(10)
                .mapToObj(n -> dynamicTest("test" + n, () -> assertTrue(n % 2 == 0)));
    }

}
解释:本例中的四个测试方法是非常简单的,演示了Collection,Iterable,Iterator或者DynamicTest实例的生成。这些测试方法中的大多数并不真正表现出动态行为,而只是在原则上展示了支持的返回类型。 而dynamicTestsFromStream()和dynamicTestsFromIntStream()演示了如何为给定的一组字符串或一组输入数字生成动态测试是如此的简单。

示例3

import org.junit.jupiter.api.DynamicNode;
import org.junit.jupiter.api.DynamicTest;
import org.junit.jupiter.api.TestFactory;
import org.junit.jupiter.api.function.ThrowingConsumer;

import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.Random;
import java.util.function.Function;
import java.util.stream.IntStream;
import java.util.stream.Stream;

import static org.junit.jupiter.api.Assertions.*;
import static org.junit.jupiter.api.DynamicContainer.dynamicContainer;
import static org.junit.jupiter.api.DynamicTest.dynamicTest;

public class DynamicTestThree {

    @TestFactory
    Stream<DynamicTest> genRandomNumber(){
        // 在0~100中随机生成正整数,直到出现7的倍数数字为止
        Iterator<Integer> inputGenerator = new Iterator<Integer>() {

            Random random = new Random();
            int current;

            @Override
            public boolean hasNext() {
                current = random.nextInt(100);
                return current % 7 != 0;
            }

            @Override
            public Integer next() {
                return current;
            }
        };
        // 生成显示名称,例如input:5, input:85等等
        Function<Integer, String> displayNameGenerator = (input) -> "input:" + input;
        // 基于当前的输入值执行测试
        ThrowingConsumer<Integer> testExecutor = (input) -> assertTrue(input % 7 != 0);
        // 返回动态测试用例流
        return DynamicTest.stream(inputGenerator, displayNameGenerator, testExecutor);
    }

}
解释:以上方法本质上是真正动态的。其中,genRandomNumber()方法实现了一个生成随机数的Iterator,一个显示名称生成器和一个测试执行器,然后将这三者全部提供给DynamicTest.stream()。尽管genRandomNumber()的非确定性行为理所当然的会与测试的可重复性相冲突,应谨慎使用,但它可以演示动态测试的表现力和力量。

示例4

import org.junit.jupiter.api.DynamicNode;
import org.junit.jupiter.api.TestFactory;

import java.util.stream.Stream;

import static org.junit.jupiter.api.Assertions.*;
import static org.junit.jupiter.api.DynamicContainer.dynamicContainer;
import static org.junit.jupiter.api.DynamicTest.dynamicTest;

public class DynamicTestFour {

    @TestFactory
    Stream<DynamicNode> withContainers() {
        return Stream.of("A", "B", "C")
                .map(input -> dynamicContainer("Container" + input,
                        Stream.of(
                                dynamicTest("not null", () -> assertNotNull(input)),
                                dynamicContainer("properties",
                                        Stream.of(
                                                dynamicTest("length > 0", () -> assertTrue(input.length() > 0)),
                                                dynamicTest("not empty", () -> assertFalse(input.isEmpty()))
                                        )
                                )
                        )
                ));
    }

}
解释:withContainers()方法使用DynamicContainer生成动态测试的嵌套层次结构。

测试模板

@TestTemplate注解

该注解的方法不是常规的测试用例,而是测试用例的模板。为此,它被设计成根据已注册的提供者返回的调用上下文的数量被调用多次。因此,它必须与注册的TestTemplateInvocationContextProvider扩展一起使用。

测试模板方法的每次调用都像执行常规@Test方法一样,完全支持相同的生命周期回调和扩展。