1. JUnit5模块组成

JUnit5包含三个模块: JUnit Platform , JUnit JupiterJUnit Vintage .

JUnit5的各个模块职责
  • JUnit Platform:

    • junit-platform-common: JUnit基础工具包.

    • junit-platform-engine: 提供TestEngine相关基础类.

    • junit-platform-launcher: 提供让客户端执行Test和收集测试结果的入口.

  • JUnit Jupiter:

    • junit-jupiter-api: JUnit Jupiter测试相关基础类/注解以及一些生命周期接口.

    • junit-jupiter-params: 提供参数化测试的扩展功能.

    • junit-jupiter-engine: 提供JUnit Jupiter的TestEngine实现.

  • JUnit Vintage: 用来兼容运行JUnit3和4的Test用例.

component diagram
Figure 1. JUnit5模块依赖图

总之, JUnit Platform 模块抽象出了一个单元测试框架的上层API, JUnit Jupiter 模块则是JUnit5针对 JUnit Platform 包具体的实现.
而JUnit Vintage模块是为了向前兼容, 让JUnit5也能执行老版本的测试代码.

2. JUnit依赖导入

pom.xml
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.junit</groupId>
                <artifactId>junit-bom</artifactId>
                <version>5.6.2</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.junit.platform</groupId>
            <artifactId>junit-platform-launcher</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

JUnit5提供了自己各个模块版本的pom包, 所以直接引入junit-bom来管理JUnit版本就行了.

3. JUnit测试方法和测试类

标有 @Test/@RepeatedTest/@ParameterizedTest/@TestFactory/@TestTemplate 注解的方法为测试方法, JUnit会在启动的时候执行这些方法. 测试方法访问修饰符可以为default.

测试类就是包含了一系列测试方法的Java类, 测试类可以有三种表现形式:

  • 标准的Java Class.

  • 静态内部类.

  • 标有@Nested注解的成员内部类.

测试类只能有一个构造方法, 且不能为抽象类. 测试类访问修饰符可以为default.

测试类中除了待执行的测试方法, 还可以包含一些测试生命周期的方法, 如标有 @BeforeEach 的方法会在每个方法执行前被执行.

4. JUnit使用

4.1. @Test

标有 @Test 注解的方法即为一个标准的测试方法.
测试方法执行抛出异常或者与断言预期不一致, 则测试方法不通过.

JUnitHelloWorld.java
@Slf4j
class JUnitHelloWorld {

    @Test
    void succeedingTest() {
        Assertions.assertTrue(true);
        log.info("succeedingTest");
    }
}

4.2. @ParameterizedTest

@ParameterizedTest用来给测试方法注入预定义的一些参数去执行, 实现了测试参数与测试代码的职责分离.

参数有三种注入形式:

  • ArgumentsProvider : 字面量和参数属于一对一的映射关系, JUnit预先提供了 @xxxSource 类注解来配和 @ParameterizedTest 使用.

  • ArgumentsAccessor : 测试方法里手动根据字面量和其索引来创建参数需要的对象.

  • ParameterResolver : 运行时动态注入参数需要的对象.

4.2.1. ArgumentsProvider

@ValueSource

@ValueSource 可以声明以下类型的字面量参数:

  • 基本数据类型: byte/short/int/long/boolean/char/float/double

  • String

  • Class

@ValueSource例子
    @ParameterizedTest
    @ValueSource(ints = {1, 2, 3, 4, 5, 6})
    void testValueSource(int i) { // 该测试方法会执行6次, 依次传入ints数组的元素
        assertTrue(i > 0);
    }
@NullSource

@NullSource 可以赋值给引用类型的参数为null.

@NullSource例子
    @ParameterizedTest
    @NullSource
    void testNullString(String nullableString) {
        assertNull(nullableString);
    }

    @ParameterizedTest
    @NullSource
    void testZeroNumber(int zero) {
        assertEquals(0, zero); // 错误, 无法将null复制给int类型的参数
    }
@EmptySource

@EmptySource 可以为参数创建一个空的值, 具体表现为:

  • String: 初始化为一个空的字符串.

  • 数组: 初始化为一个长度为0的数组.

  • 集合: 初始化为Collections.emptyXXX()方法返回的空集合, 如 List 参数会被初始化为 Collections.emptyList() 方法返回的对象.

@EmptySource例子
    @ParameterizedTest
    @EmptySource
    void testEmptyString(String str) {
        assertEquals(0, str.length());
    }

    @ParameterizedTest
    @EmptySource
    void testEmptyList(List<String> list) {
        assertSame(Collections.emptyList(), list);
        assertEquals(0, list.size());
    }

    @ParameterizedTest
    @EmptySource
    void testEmptySet(Set<String> set) {
        assertSame(Collections.emptySet(), set);
        assertEquals(0, set.size());
    }

    @ParameterizedTest
    @EmptySource
    void testEmptyMap(Map<String, Object> map) {
        assertSame(Collections.emptyMap(), map);
        assertEquals(0, map.size());
    }

    @ParameterizedTest
    @EmptySource
    void testEmptyArray(int[] arr) {
        assertEquals(0, arr.length);
    }
@NullAndEmptySource

@NullAndEmptySource 注解是 @NullSource@EmptySource 两个注解的组合: 会分别将方法参数注入一个null和一个空对象, 也就是说测试方法会被执行两次.
可以用来测试方法的鲁棒性👀.

@EnumSource

@EnumSource 用来注入枚举类参数.

@EnumSource例子
    @ParameterizedTest
    @EnumSource
    void testEnumSource(Gender gender) {
        assertTrue(Arrays.stream(Gender.class.getEnumConstants()).anyMatch(e -> e == gender)); // 该方法会执行3次, 分别注入Gender的三个枚举值.
    }

    public enum Gender {
        MALE, FEMALE, UNKNOWN
    }

@EnumSource 也可以通过设置 namesmode 属性来过滤注入的枚举值.

@MethodSource

@MethodSource 用来通过方法返回值来注入参数.
methodSource方法需要为static.
方法的返回值需要为 Stream/Collection/Iterator/Iterable/数组 类型.
如果泛型为 Arguments 类型, 则可以同时注入多个参数.

MethodSource注入单个参数例子
    @ParameterizedTest
    @MethodSource("generateInts")
    void testIntMethodSource(int i) { // 注入1到9
        assertTrue(i > 0 && i < 10);
    }

    static IntStream generateInts() {
        return IntStream.range(1, 10)
    }
MethodSource注入多个参数例子
    @ParameterizedTest
    @MethodSource("generateArguments")
    void testArgumentsMethodSource(String str, int i) {
        assertEquals(i, str.length());
    }

    static Stream<Arguments> generateArguments() {
        return Stream.of(
            Arguments.of("a", 1),
            Arguments.of("aa", 2),
            Arguments.of("aaa", 3)
        );
    }
@CsvSource

@CsvSource 可以同时注入多个字面量参数.

@CsvSource例子
    @ParameterizedTest
    @CsvSource({"a,1", "aa,2", "aaa,3"})
    void testCsvSource(String str, int i) {
        assertEquals(i, str.length());
    }
@CsvFileSource

@CsvFileSource 可以读取csv文件, 然后注入字面量参数.

@CsvFileSource例子
    @ParameterizedTest
    @CsvFileSource(resources = "/str.csv")
    void testCsvFileSource(String str, int i) {
        assertEquals(i, str.length());
    }
str.csv
a,1
aa,2
aaa,3
@CsvFileSource的几点使用心得:
  • 如果csv文件第一行为表头, 可以设置 numLinesToSkip = 1 来过滤掉第一行.

  • 如果某一列里面包含逗号, 会导致csv解析出现异常, 可以通过设置 delimiterString 来区分列.

@ArgumentsSource

@ArgumentsSource 可以指定一个 ArgumentsProvider 的实现类来注入参数.

@ArgumentsSource例子
    @ParameterizedTest
    @ArgumentsSource(SequenceArgumentProvider.class)
    void testArgumentsSource(int i) {
        assertTrue(i > 0 && i < 10);
    }

    public static class SequenceArgumentProvider implements ArgumentsProvider{
        @Override
        public Stream<Arguments> provideArguments(ExtensionContext context) {
            return IntStream.range(1, 10).mapToObj(Arguments::of);
        }
    }
参数类型转换

字面量和参数的类型转换分为隐式类型转换和显示类型转换.

ArgumentConverter例子
    @ParameterizedTest
    @CsvSource({"'1,3,2', '1,2,3'"})
    void testConverter(@ConvertWith(ToArrayArgumentConverter.class) int[] arr,
                       @ConvertWith(ToArrayArgumentConverter.class) int[] expect) {
        Arrays.sort(arr);
        assertArrayEquals(expect, arr);
    }

    public static class ToArrayArgumentConverter implements ArgumentConverter {

        @Override
        public Object convert(Object source, ParameterContext context) throws ArgumentConversionException {
            Class<?> type = context.getParameter().getType();
            String[] strings = source.toString().split("\\s*,\\s*");
            if (int[].class == type) {
                return Arrays.stream(strings).mapToInt(Integer::valueOf).toArray();
            }
            return strings;
        }
    }

4.2.2. ArgumentAccessor

ArgumentAccessor 可以通过获取指定位置的参数来在方法内部获取参数值.
有两种使用方式:

  • ArgumentAccessor 作为参数, 然后在方法内部使用.

  • 实现 ArgumentsAggregator 接口, 使用 @AggregateWith 注解指定 ArgumentsAggregator 来实现参数类型转换.

ArgumentAccessor参数例子
    @ParameterizedTest
    @CsvSource({"1", "2", "3", "4", "5", "6"})
    void testWithArgumentsAccessor(ArgumentsAccessor argumentsAccessor) {
        Integer i = argumentsAccessor.getInteger(0);
        assertTrue(i > 0);
    }
AggregateWith注解例子
    @ParameterizedTest
    @CsvSource({"1", "2", "3", "4", "5", "6"})
    void testWithArgumentsAccessor(@AggregateWith(ToIntArgumentsAggregator.class) int i) {
        assertTrue(i > 0);
    }

    public static class ToIntArgumentsAggregator implements ArgumentsAggregator {
        @Override
        public Object aggregateArguments(ArgumentsAccessor accessor, ParameterContext context) throws ArgumentsAggregationException {
            return accessor.getInteger(0);
        }
    }

4.2.3. ParameterResolver

JUnit5 允许使用外部扩展的方式来注入参数值.

RandomInt.java
    @Target(ElementType.PARAMETER)
    @Retention(RetentionPolicy.RUNTIME)
    @Inherited
    @Documented
    public @interface RandomInt {
    }
DemoExtension.java
public class DemoExtension implements ParameterResolver {
    @Override
    public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException {
        return parameterContext.isAnnotated(RandomInt.class);
    }

    @Override
    public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException {
        return ThreadLocalRandom.current().nextInt();
    }
}
DemoTest.java
@ExtendWith(DemoExtension.class)
public class DemoTest {
    @Test
    void testRandomInt(@RandomInt int i) {
        // i被设置成随机数
    }
}

4.3. @RepeatedTest

@RepeatedTest 可以让JUnit重复执行测试方法.
方法参数可以注入一个运行时的 RepetitionInfo 对象来让方法内部获取到重复执行的序号和总次数.

@RepeatedTest例子
    @RepeatedTest(3)
    void testWithRepeatedTest() {
        assertTrue(true); // 方法会执行3次
    }

4.4. @TestFactory

@TestFactory 可以像 @MethodSource 一样以编程式的方式执行测试用例.
定义一个方法, 返回Stream/Collection/Iterator/Iterable/数组类型, 泛型类型需要为 DynamicTest .

@TestFactory例子
    @TestFactory
    Stream<DynamicTest> dynamicTestStream() {
        return Stream.of("a", "b", "c")
            .map(text -> DynamicTest.dynamicTest(text, () -> assertTrue(true)));
    }

4.5. @TestMethodOrder

@TestMethodOrder 可以指定测试方法的执行顺序:

  • Alphanumeric : 按照测试方法名和参数列表字母排序执行.

  • OrderAnnotation : 按照测试方法上的 @Order 注解指定的顺序执行, 如果没有注解则默认为 Integer.MAX_VALUE / 2.

  • Random : 随机顺序执行.

4.6. @TestInstance

默认情况下, 每次执行测试方法时都会新创建测试类的一个实例, 等同于 @TestInstance(PER_METHOD)

class JUnitApiTest {

    private int i;

    @RepeatedTest(10)
    void testPerMethod1() {
        i++;
        // 每次都会在一个新的实例中执行该方法, 所以i++均为1.
        assertEquals(1, i);
    }
}

@TestInstance(PER_CLASS) 情况下, 测试类只会实例化一次.
此外 PER_CLASS 下 `@BeforeAll@AfterAll 注解可以用在类的实例方法或者接口的default方法上.

4.7. @Execution

@Execution 可以设置测试方法在同一个线程中执行, 还是使用ForkJoin线程池并行执行.

除了使用 @Execution 注解, 还可以在 junit-platform.properties 中全局配置.

4.7.1. 测试类并行执行但同一类方法顺序执行

junit.jupiter.execution.parallel.enabled = true
junit.jupiter.execution.parallel.mode.default = same_thread
junit.jupiter.execution.parallel.mode.classes.default = concurrent

4.7.2. 测试类顺序执行但同一类方法并行执行

junit.jupiter.execution.parallel.enabled = true
junit.jupiter.execution.parallel.mode.default = concurrent
junit.jupiter.execution.parallel.mode.classes.default = same_thread

4.8. 条件执行

可以指定测试方法在特定环境下才执行.

ExecutionCondition例子
    @Test
    @EnabledOnJre(JAVA_8)
    void onlyOnJava8() {
    // Java8 才执行
    }

本质上是执行了 ExecutionCondition::evaluateExecutionCondition 方法来判断是否执行.

5. Extension机制

JUnit 提供了扩展接口, 来在测试方法执行前后执行一些自定义的回调.
Extension的功能主要包括:

  • 对实例化测试类对象时后置处理.

  • 测试类执行条件判断.

  • 生命周期回调.

  • 自定义参数解析.

  • 异常处理.

5.1. 注册Extension的方式

5.1.1. 注解

@ExtendWith(DemoExtension.class)
class ExtensionTest {
}

5.1.2. SPI

  1. /META-INF/services/org.junit.jupiter.api.extension.Extension 文件里添加自定义的Extension类的全限定名.

  2. junit-platform.properties 里添加 junit.jupiter.extensions.autodetection.enabled=true .

5.1.3. @RegisterExtension注解

  • @RegisterExtension 注解static字段.

  • @RegisterExtension 注解实例字段.

5.2. Extension的生命周期

  1. BeforeAllCallback

  2. @BeforeAll

  3. TestInstancePostProcessor

  4. BeforeEachCallback

  5. @BeforeEach

  6. BeforeTestExecutionCallback

  7. @Test

  8. AfterTestExecutionCallback

  9. @AfterEach

  10. AfterEachCallback

  11. @AfterAll

  12. AfterAllCallback