单元测试 - Junit5 详解

JUnit 5是JUnit的下一代。目标是为JVM上的开发人员端测试创建一个最新的基础。这包括专注于Java 8及更高版本,以及启用许多不同风格的测试。@pdai

官方资料

最好的资料依然在Junit官方网站,以下我帮你总结下Junit相关的官方网址。@pdai

  • 官网地址

https://junit.org/junit5/

  • 官方入门文档

https://junit.org/junit5/docs/current/user-guide/#overview

  • 官方例子

https://github.com/junit-team/junit5-samples

  • 官方github

https://github.com/junit-team

Junit5的架构

与以前版本的JUnit不同,JUnit 5由三个不同子项目中的几个不同模块组成。

JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage

  • JUnit Platform是基于JVM的运行测试的基础框架在,它定义了开发运行在这个测试框架上的TestEngine API。此外该平台提供了一个控制台启动器,可以从命令行启动平台,可以为Gradle和 Maven构建插件,同时提供基于JUnit 4的Runner。
  • JUnit Jupiter是在JUnit 5中编写测试和扩展的新编程模型和扩展模型的组合.Jupiter子项目提供了一个TestEngine在平台上运行基于Jupiter的测试。
  • JUnit Vintage提供了一个TestEngine在平台上运行基于JUnit 3和JUnit 4的测试。

架构图如下:

JUnit Jupiter API 的使用

JUnit Jupiter是在JUnit 5中编写测试和扩展的新编程模型和扩展模型的组合; 所以我们使用Jupiter来学习Junit5。

常用注解

@Test 表示方法是一种测试方法。 与JUnit 4的@Test注解不同,此注释不会声明任何属性。

@ParameterizedTest 表示方法是参数化测试

@RepeatedTest 表示方法是重复测试模板

@TestFactory 表示方法是动态测试的测试工程

@DisplayName 为测试类或者测试方法自定义一个名称

@BeforeEach 表示方法在每个测试方法运行前都会运行 ,@AfterEach 表示方法在每个测试方法运行之后都会运行

@BeforeAll 表示方法在所有测试方法之前运行 ,@AfterAll 表示方法在所有测试方法之后运行

@Nested 表示带注解的类是嵌套的非静态测试类,@BeforeAll@AfterAll方法不能直接在@Nested测试类中使用,除非修改测试实例生命周期。

@Tag 用于在类或方法级别声明用于过滤测试的标记

@Disabled 用于禁用测试类或测试方法

@ExtendWith 用于注册自定义扩展,该注解可以继承

@FixMethodOrder(MethodSorters.NAME_ASCENDING),控制测试类中方法执行的顺序,这种测试方式将按方法名称的进行排序,由于是按字符的字典顺序,所以以这种方式指定执行顺序会始终保持一致;不过这种方式需要对测试方法有一定的命名规则,如 测试方法均以testNNN开头(NNN表示测试方法序列号 001-999)

编写单元测试

接下来,我们开始学习JUnit5单元测试实例:

Maven包引入

最新的包引入,请参考这里:https://junit.org/junit5/docs/current/user-guide/#running-tests

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>pdai.tech</groupId>
    <artifactId>java-junit5</artifactId>
    <version>1.0-SNAPSHOT</version>

    <dependencies>
        <!-- Only needed to run tests in a version of IntelliJ IDEA that bundles older versions -->
        <dependency>
            <groupId>org.junit.platform</groupId>
            <artifactId>junit-platform-launcher</artifactId>
            <version>1.7.0</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-engine</artifactId>
            <version>5.7.0</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.junit.vintage</groupId>
            <artifactId>junit-vintage-engine</artifactId>
            <version>5.7.0</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-api</artifactId>
            <version>5.7.0</version>
        </dependency>

        <!-- lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.16</version>
        </dependency>
    </dependencies>

</project>

测试:Hello World

第一个测试:

package tech.pdai.junit5;

import org.junit.jupiter.api.Test;

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

/**
 * Hello world test.
 *
 * @author pdai
 */
public class HelloWorldTest {

    @Test
    void firstTest() {
        assertEquals(2, 1 + 1);
    }
}

执行结果

@Test注解在方法上标记方法为测试方法,以便构建工具和 IDE 能够识别并执行它们。JUnit 5不再需要手动将测试类与测试方法为public,包可见的访问级别就足够了。

测试:生命周期

首先,需要对比下Junit5和Junit4注解:

Junit4Junit5注释
@Test@Test表示该方法是一个测试方法
@BeforeClass@BeforeAll表示使用了该注解的方法应该在当前类中所有测试方法之前执行(只执行一次),并且它必须是 static方法(除非@TestInstance指定生命周期为Lifecycle.PER_CLASS)
@AfterClass@AfterAll表示使用了该注解的方法应该在当前类中所有测试方法之后执行(只执行一次),并且它必须是 static方法(除非@TestInstance指定生命周期为Lifecycle.PER_CLASS)
@Before@BeforeEach表示使用了该注解的方法应该在当前类中每一个测试方法之前执行
@After@AfterEach表示使用了该注解的方法应该在当前类中每一个测试方法之后执行
@Ignore@Disabled用于禁用(或者说忽略)一个测试类或测试方法
@Category@Tag用于声明过滤测试的tag标签,该注解可以用在方法或类上

测试用例:

package tech.pdai.junit5;

import static org.junit.jupiter.api.Assertions.fail;
import static org.junit.jupiter.api.Assumptions.assumeTrue;

import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;

/**
 * Standard Test.
 *
 * @author pdai
 */
public class StandardTest {

    @BeforeAll
    static void initAll() {
        System.out.println("BeforeAll");
    }

    @BeforeEach
    void init() {
        System.out.println("BeforeEach");
    }

    @Test
    void succeedingTest() {
        System.out.println("succeedingTest");
    }

    @Test
    void failingTest() {
        System.out.println("failingTest");
        fail("a failing test");
    }

    @Test
    @Disabled("for demonstration purposes")
    void skippedTest() {
        // not executed
    }

    @Test
    void abortedTest() {
        System.out.println("abortedTest");
        assumeTrue("abc".contains("Z"));
        fail("test should have been aborted");
    }

    @AfterEach
    void tearDown() {
        System.out.println("AfterEach");
    }

    @AfterAll
    static void tearDownAll() {
        System.out.println("AfterEach");
    }

}

执行结果

观察正确和错误的结果:

BeforeAll

BeforeEach
succeedingTest
AfterEach


BeforeEach
failingTest
AfterEach


org.opentest4j.AssertionFailedError: a failing test
  at org.junit.jupiter.api.AssertionUtils.fail(AssertionUtils.java:39)
  // ...


BeforeEach
abortedTest
AfterEach


org.opentest4j.TestAbortedException: Assumption failed: assumption is not true
	at org.junit.jupiter.api.Assumptions.throwTestAbortedException(Assumptions.java:256)
  // ...

AfterEach

Process finished with exit code 255

测试:禁用测试

这是一个禁用的测试案例:

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

@Disabled
class DisabledClassTest {
    @Test
    void testWillBeSkipped() {
    }
}

这是一个带有禁用测试方法的测试案例:

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

class DisabledTest {

    @Disabled
    @Test
    void testWillBeSkipped() {
    }

    @Test
    void testWillBeExecuted() {
    }
}

测试:断言测试

准备好测试实例、执行了被测类的方法以后,断言能确保你得到了想要的结果。一般的断言,无非是检查一个实例的属性(比如,判空与判非空等),或者对两个实例进行比较(比如,检查两个实例对象是否相等)等。无论哪种检查,断言方法都可以接受一个字符串作为最后一个可选参数,它会在断言失败时提供必要的描述信息。如果提供出错信息的过程比较复杂,它也可以被包装在一个 lambda 表达式中,这样,只有到真正失败的时候,消息才会真正被构造出来。

  • 常用断言 Assertions
    • assertEquals 断言预期值和实际值相等
    • assertAll 分组断言,执行其中包含的所有断言
    • assertArrayEquals 断言预期数组和实际数组相等
    • assertFalse 断言条件为假
    • assertNotNull 断言不为空
    • assertSame 断言两个对象相等
    • assertTimeout 断言超时
    • fail 使单元测试失败

定义一个Person实体类

package tech.pdai.junit5.entity;

import lombok.AllArgsConstructor;
import lombok.Data;

/**
 * Person.
 *
 * @author pdai
 */
@Data
@AllArgsConstructor
public class Person {

    private String firstName;

    private String lastName;
}

测试代码:

package tech.pdai.junit5;

import org.junit.jupiter.api.Test;
import tech.pdai.junit5.entity.Person;

import static java.time.Duration.ofMillis;
import static java.time.Duration.ofMinutes;
import static org.junit.jupiter.api.Assertions.*;

/**
 * Assertions Test.
 *
 */
public class AssertionsTest {

    Person person = new Person("John", "Doe");

    @Test
    void standardAssertions() {
        assertEquals(2, 2);
        assertEquals(4, 4, "The optional assertion message is now the last parameter.");
        assertTrue(2 == 2, () -> "Assertion messages can be lazily evaluated -- "
                + "to avoid constructing complex messages unnecessarily.");
    }

    @Test
    void groupedAssertions() {
        // In a grouped assertion all assertions are executed, and any
        // failures will be reported together.
        assertAll("person",
                () -> assertEquals("John", person.getFirstName()),
                () -> assertEquals("Doe", person.getLastName())
        );
    }

    @Test
    void dependentAssertions() {
        // Within a code block, if an assertion fails the
        // subsequent code in the same block will be skipped.
        assertAll("properties",
                () -> {
                    String firstName = person.getFirstName();
                    assertNotNull(firstName);

                    // Executed only if the previous assertion is valid.
                    assertAll("first name",
                            () -> assertTrue(firstName.startsWith("J")),
                            () -> assertTrue(firstName.endsWith("n"))
                    );
                },
                () -> {
                    // Grouped assertion, so processed independently
                    // of results of first name assertions.
                    String lastName = person.getLastName();
                    assertNotNull(lastName);

                    // Executed only if the previous assertion is valid.
                    assertAll("last name",
                            () -> assertTrue(lastName.startsWith("D")),
                            () -> assertTrue(lastName.endsWith("e"))
                    );
                }
        );
    }

    @Test
    void exceptionTesting() {
        Throwable exception = assertThrows(IllegalArgumentException.class, () -> {
            throw new IllegalArgumentException("a message");
        });
        assertEquals("a message", exception.getMessage());
    }

    @Test
    void timeoutNotExceeded() {
        // The following assertion succeeds.
        assertTimeout(ofMinutes(2), () -> {
            // Perform task that takes less than 2 minutes.
        });
    }

    @Test
    void timeoutNotExceededWithResult() {
        // The following assertion succeeds, and returns the supplied object.
        String actualResult = assertTimeout(ofMinutes(2), () -> {
            return "a result";
        });
        assertEquals("a result", actualResult);
    }

    @Test
    void timeoutNotExceededWithMethod() {
        // The following assertion invokes a method reference and returns an object.
        String actualGreeting = assertTimeout(ofMinutes(2), AssertionsTest::greeting);
        assertEquals("hello world!", actualGreeting);
    }

    @Test
    void timeoutExceeded() {
        // The following assertion fails with an error message similar to:
        // execution exceeded timeout of 10 ms by 91 ms
        assertTimeout(ofMillis(10), () -> {
            // Simulate task that takes more than 10 ms.
            Thread.sleep(100);
        });
    }

    @Test
    void timeoutExceededWithPreemptiveTermination() {
        // The following assertion fails with an error message similar to:
        // execution timed out after 10 ms
        assertTimeoutPreemptively(ofMillis(10), () -> {
            // Simulate task that takes more than 10 ms.
            Thread.sleep(100);
        });
    }

    private static String greeting() {
        return "hello world!";
    }
}

这里注意下:assertTimeoutPreemptively()assertTimeout() 的区别为: 两者都是断言超时,前者在指定时间没有完成任务就会立即返回断言失败;后者会在任务执行完毕之后才返回。

执行结果:

观察错误的结果:

org.opentest4j.AssertionFailedError: execution timed out after 10 ms
	at org.junit.jupiter.api.AssertTimeout.assertTimeoutPreemptively(AssertTimeout.java:158)
	at org.junit.jupiter.api.AssertTimeout.assertTimeoutPreemptively(AssertTimeout.java:119)
	at org.junit.jupiter.api.AssertTimeout.assertTimeoutPreemptively(AssertTimeout.java:101)
	at org.junit.jupiter.api.AssertTimeout.assertTimeoutPreemptively(AssertTimeout.java:97)
	at org.junit.jupiter.api.Assertions.assertTimeoutPreemptively(Assertions.java:3323)
	at tech.pdai.junit5.AssertionsTest.timeoutExceededWithPreemptiveTermination(AssertionsTest.java:108)
  // ...

org.opentest4j.AssertionFailedError: execution exceeded timeout of 10 ms by 92 ms
	at org.junit.jupiter.api.AssertionUtils.fail(AssertionUtils.java:39)
	at org.junit.jupiter.api.Assertions.fail(Assertions.java:117)
	at org.junit.jupiter.api.AssertTimeout.assertTimeout(AssertTimeout.java:90)
	at org.junit.jupiter.api.AssertTimeout.assertTimeout(AssertTimeout.java:70)
	at org.junit.jupiter.api.AssertTimeout.assertTimeout(AssertTimeout.java:52)
	at org.junit.jupiter.api.AssertTimeout.assertTimeout(AssertTimeout.java:48)
	at org.junit.jupiter.api.Assertions.assertTimeout(Assertions.java:3186)
	at tech.pdai.junit5.AssertionsTest.timeoutExceeded(AssertionsTest.java:98)
  // ...


Process finished with exit code 255

测试:异常测试

我们代码中对于带有异常的方法通常都是使用 try-catch 方式捕获处理,针对测试这样带有异常抛出的代码,而 JUnit 5 提供方法 Assertions#assertThrows(Class<T>, Executable) 来进行测试,第一个参数为异常类型,第二个为函数式接口参数,跟 Runnable 接口相似,不需要参数,也没有返回,并且支持 Lambda表达式方式使用,具体使用方式可参考下方代码:

package tech.pdai.junit5;

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

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

/**
 * Exception Test.
 */
public class ExceptionTest {

    // 标准的测试例子
    @Test
    @DisplayName("Exception Test Demo")
    void assertThrowsException() {
        String str = null;
        assertThrows(IllegalArgumentException.class, () -> {
            Integer.valueOf(str);
        });
    }

    // 注:异常失败例子,当Lambda表达式中代码出现的异常会跟首个参数的异常类型进行比较,如果不属于同一类异常,则失败
    @Test
    @DisplayName("Exception Test Demo2")
    void assertThrowsException2() {
        String str = null;
        assertThrows(NullPointerException.class, () -> {
            Integer.valueOf(str);
        });
    }
}

执行结果:

观察错误的结果:

org.opentest4j.AssertionFailedError: Unexpected exception type thrown ==> expected: <java.lang.NullPointerException> but was: <java.lang.NumberFormatException>

	at org.junit.jupiter.api.AssertThrows.assertThrows(AssertThrows.java:65)
	at org.junit.jupiter.api.AssertThrows.assertThrows(AssertThrows.java:37)
	at org.junit.jupiter.api.Assertions.assertThrows(Assertions.java:3007)
	at tech.pdai.junit5.ExceptionTest.assertThrowsException2(ExceptionTest.java:26)
  // ...
Caused by: java.lang.NumberFormatException: null
	at java.lang.Integer.parseInt(Integer.java:542)
	at java.lang.Integer.valueOf(Integer.java:766)
	at tech.pdai.junit5.ExceptionTest.lambda$assertThrowsException2$1(ExceptionTest.java:27)
	at org.junit.jupiter.api.AssertThrows.assertThrows(AssertThrows.java:55)
	... 68 more

测试:嵌套测试

嵌套测试给测试编写者更多的能力,来表达几组测试之间的关系。这里有一个详细的例子。

用于测试stack的嵌套测试套件:

package tech.pdai.junit5;

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

import java.util.EmptyStackException;
import java.util.Stack;

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

/**
 * Stack test for Nest Demo.
 */
@DisplayName("A stack")
public class NestedTest {

    Stack stack;

    @Test
    @DisplayName("is instantiated with new Stack()")
    void isInstantiatedWithNew() {
        new Stack<>();
    }

    @Nested
    @DisplayName("when new")
    class WhenNew {
        @BeforeEach
        void createNewStack() {
            stack = new Stack<>();
        }

        @Test
        @DisplayName("is empty")
        void isEmpty() {
            assertTrue(stack.isEmpty());
        }

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

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

        @Nested
        @DisplayName("after pushing an element")
        class AfterPushing {
            String anElement = "an element";

            @BeforeEach
            void pushAnElement() {
                stack.push(anElement);
            }

            @Test
            @DisplayName("it is no longer empty")
            void isNotEmpty() {
                assertFalse(stack.isEmpty());
            }

            @Test
            @DisplayName("returns the element when popped and is empty")
            void returnElementWhenPopped() {
                assertEquals(anElement, stack.pop());
                assertTrue(stack.isEmpty());
            }

            @Test
            @DisplayName("returns the element when peeked but remains not empty")
            void returnElementWhenPeeked() {
                assertEquals(anElement, stack.peek());
                assertFalse(stack.isEmpty());
            }
        }
    }
}

执行结果:

测试:重复测试

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

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

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

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

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

测试例子

package tech.pdai.junit5;

import org.junit.jupiter.api.*;

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

/**
 * Repeat Test.
 */
public class RepeatTest {

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

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

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

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

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

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

执行结果:

测试:参数化测试

JUnit Jupiter开箱即用,提供了不少source注解。下面的每个小节都为他们提供了简要的概述和示例。请参阅org.junit.jupiter.params.provider包中的JavaDoc以获取更多信息。

  • @ValueSource

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

@ParameterizedTest
@ValueSource(ints = { 1, 2, 3 })
void testWithValueSource(int argument) {
    assertNotNull(argument);
}
  • @EnumSource

@EnumSource提供了一个使用Enum常量的简便方法。该注释提供了一个可选的name参数,可以指定使用哪些常量。如果省略,所有的常量将被用在下面的例子中。

@ParameterizedTest
@EnumSource(TimeUnit.class)
void testWithEnumSource(TimeUnit timeUnit) {
    assertNotNull(timeUnit);
}
@ParameterizedTest
@EnumSource(value = TimeUnit.class, names = { "DAYS", "HOURS" })
void testWithEnumSourceInclude(TimeUnit timeUnit) {
    assertTrue(EnumSet.of(TimeUnit.DAYS, TimeUnit.HOURS).contains(timeUnit));
}

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

@ParameterizedTest
@EnumSource(value = TimeUnit.class, mode = EXCLUDE, names = { "DAYS", "HOURS" })
void testWithEnumSourceExclude(TimeUnit timeUnit) {
    assertFalse(EnumSet.of(TimeUnit.DAYS, TimeUnit.HOURS).contains(timeUnit));
    assertTrue(timeUnit.name().length() > 5);
}
@ParameterizedTest
@EnumSource(value = TimeUnit.class, mode = MATCH_ALL, names = "^(M|N).+SECONDS$")
void testWithEnumSourceRegex(TimeUnit timeUnit) {
    String name = timeUnit.name();
    assertTrue(name.startsWith("M") || name.startsWith("N"));
    assertTrue(name.endsWith("SECONDS"));
}
  • @MethodSource

@MethodSource允许你引用一个或多个测试类的工厂方法。这样的方法必须返回一个Stream,Iterable,Iterator或者参数数组。另外,这种方法不能接受任何参数。默认情况下,除非测试类用@TestInstance(Lifecycle.PER_CLASS)注解,否则这些方法必须是静态的。

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

@ParameterizedTest
@MethodSource("stringProvider")
void testWithSimpleMethodSource(String argument) {
    assertNotNull(argument);
}
static Stream<String> stringProvider() {
    return Stream.of("foo", "bar");
}

支持原始类型(DoubleStream,IntStream和LongStream)的流,示例如下:

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

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

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

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

@ParameterizedTest
@CsvSource({ "foo, 1", "bar, 2", "'baz, qux', 3" })
void testWithCsvSource(String first, int second) {
    assertNotNull(first);
    assertNotEquals(0, second);
}

@CsvSource使用'作为转义字符。 请参阅上述示例和下表中的’baz, qux’值。 一个空的引用值''会导致一个空的String; 而一个完全空的值被解释为一个null引用。如果null引用的目标类型是基本类型,则引发ArgumentConversionException。

示例输入结果字符列表
@CsvSource({ “foo, bar” })"foo", "bar"
@CsvSource({ “foo, ‘baz, qux’” })"foo", "baz, qux"
@CsvSource({ “foo, ‘’” })"foo", ""
@CsvSource({ “foo, “ })"foo", null
  • @CsvFileSource

@CsvFileSource让你使用classpath中的CSV文件。CSV文件中的每一行都会导致参数化测试的一次调用。

@ParameterizedTest
@CsvFileSource(resources = "/two-column.csv")
void testWithCsvFileSource(String first, int second) {
    assertNotNull(first);
    assertNotEquals(0, second);
}

two-column.csv

foo, 1
bar, 2
"baz, qux", 3

与@CsvSource中使用的语法相反,@CsvFileSource使用双引号"作为转义字符,请参阅上面例子中的"baz, qux"值,一个空的转义值""会产生一个空字符串, 一个完全为空的值被解释为null引用,如果null引用的目标类型是基本类型,则引发ArgumentConversionException。

  • @ArgumentsSource

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

@ParameterizedTest
@ArgumentsSource(MyArgumentsProvider.class)
void testWithArgumentsSource(String argument) {
    assertNotNull(argument);
}
static class MyArgumentsProvider implements ArgumentsProvider {
    @Override
    public Stream< ? extends Arguments > provideArguments(ExtensionContext context) {
        return Stream.of("foo", "bar").map(Arguments::of);
    }
}

执行结果:

测试:动态测试

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

与@Test方法相比,@TestFactory方法本身不是测试用例,而是测试用例的工厂。因此,动态测试是工厂的产物。从技术上讲,@TestFactory方法必须返回DynamicNode实例的Stream,Collection,Iterable或Iterator。 DynamicNode的可实例化的子类是DynamicContainer和DynamicTest。 DynamicContainer实例由一个显示名称和一个动态子节点列表组成,可以创建任意嵌套的动态节点层次结构。然后,DynamicTest实例将被延迟执行,从而实现测试用例的动态甚至非确定性生成。

任何由@TestFactory返回的Stream都要通过调用stream.close()来正确关闭,使得使用诸如Files.lines()之类的资源变得安全。

与@Test方法一样,@TestFactory方法不能是private或static,并且可以选择声明参数,以便通过ParameterResolvers解析。

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

package tech.pdai.junit5;

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.*;
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;

/**
 * Dynamic Test.
 */
public class DynamicsTest {

    // This will result in a JUnitException!
    @TestFactory
    List<String> dynamicTestsWithInvalidReturnType() {
        return Arrays.asList("Hello");
    }

    @TestFactory
    Collection<DynamicTest> dynamicTestsFromCollection() {
        return Arrays.asList(
                dynamicTest("1st dynamic test", () -> assertTrue(true)),
                dynamicTest("2nd dynamic test", () -> assertEquals(4, 2 * 2))
        );
    }

    @TestFactory
    Iterable<DynamicTest> dynamicTestsFromIterable() {
        return Arrays.asList(
                dynamicTest("3rd dynamic test", () -> assertTrue(true)),
                dynamicTest("4th dynamic test", () -> assertEquals(4, 2 * 2))
        );
    }

    @TestFactory
    Iterator<DynamicTest> dynamicTestsFromIterator() {
        return Arrays.asList(
                dynamicTest("5th dynamic test", () -> assertTrue(true)),
                dynamicTest("6th dynamic test", () -> assertEquals(4, 2 * 2))
        ).iterator();
    }

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

    @TestFactory
    Stream<DynamicTest> dynamicTestsFromIntStream() {
        // Generates tests for the first 10 even integers.
        return IntStream.iterate(0, n -> n + 2).limit(10)
                .mapToObj(n -> dynamicTest("test" + n, () -> assertTrue(n % 2 == 0)));
    }

    @TestFactory
    Stream<DynamicTest> generateRandomNumberOfTests() {
        // Generates random positive integers between 0 and 100 until
        // a number evenly divisible by 7 is encountered.
        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;
            }
        };
        // Generates display names like: input:5, input:37, input:85, etc.
        Function<Integer, String> displayNameGenerator = (input) -> "input:" + input;
        // Executes tests based on the current input value.
        ThrowingConsumer<Integer> testExecutor = (input) -> assertTrue(input % 7 != 0);
        // Returns a stream of dynamic tests.
        return DynamicTest.stream(inputGenerator, displayNameGenerator, testExecutor);
    }

    @TestFactory
    Stream<DynamicNode> dynamicTestsWithContainers() {
        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()))
                        ))
                )));
    }
}

执行结果:

参考文章

  • https://junit.org/junit5/
  • https://github.com/junit-team
  • https://junit.org/junit5/docs/current/user-guide
  • https://www.bookstack.cn/read/junit5
  • https://blog.csdn.net/swordcenter/article/details/79279094
  • https://www.cnblogs.com/one12138/archive/2019/09/17/11536492.html