# 09. JUnit5

# 1. JUnit5 注解

注解 说明
@Test表示 方法是测试方法(与JUnit4的@Test不同,它的职责非常单一,不能声明任何属性,拓展的测试将会由Jupiter提供额外注解)
@ParameterizedTest 表示方法是参数化测试
@RepeatedTest 表示方法可重复执行
@DisplayName 为测试类或者测试方法设置展示名称
@BeforeEach 表示在每个测试方法之前执行
@AfterEach 表示在每个测试方法之后执行
@BeforeAll 只执行一次,执行时机是在所有测试方法和@BeforeEach注解方法之前
@AfterAll 只执行一次,执行时机是在所有测试方法和@AfterEach注解方法之后
@Tag 表示单元测试类别。类似于JUnit4中的@Categories
@Disabled 表示测试类或测试方法不执行。类似于JUnit4中的@Ignore
@Timeout 表示测试方法运行如果超过了执行时间将会返回错误
@ExtendWith 为测试类或测试方法提供扩展类引用

# 0) @ExtendWith

# 1) @ParameterizedTest 参数化测试

在 JUnit4 中,如果想要实现参数化测试(使用不同的参数来测试相同的一个方法),只能使用测试类中的字段来实现。而在 JUnit5 中,提供了参数化测试来实现这个需求。不同的参数值可以直接和一个测试方法关联,并且允许直接在一个测试类中提供不同的参数值直接参与测试,这些在 JUnit4 中都是无法实现的。

JUnit5 的参数化可以通过一组 CSV 格式的字符串、外部的 CSV、YML、JSON 文件、枚举、工厂方法,或者指定的提供类来提供。CSV 中的字符串类型的值还可以自动地转化为指定的类型,并且可以完成自己的类型转换器,如将 String 转成你希望的任何指定类型。

  • @ValueSource:指定入参来源,支持八大基础类、String、Class 类型

  • @NullSource:提供一个 null 入参

  • @EnumSource:提供一个枚举入参

  • @MethodSource:通过一个方法入参(该方法实现了自定义的数据获取方式)

  • @CsvSource:提供 CSV 格式的入参

  • @CsvFileSource:通过 CSV 文件提供参数

  • @ArgumentsSource

# 简单参数与 CSV 参数

public class ParameterTest {

    @DisplayName("Parameter Count : 1")
    @ParameterizedTest
    @ValueSource(ints = {1, 2, 3})
    void test1(int num1) {
        assertTrue(num1 < 4);
    }

    @DisplayName("Parameter Count : 2")
    @ParameterizedTest(name = "{index} ==> fruit=''{0}'', qty={1}")
    @CsvSource({
            "apple, 1",
            "banana, 2"
    })
    void test2(String fruit, int qty) {
        assertTrue(true);
    }

    @DisplayName("Parameter Count : 3")
    @ParameterizedTest(name = "{index} ==> fruit=''{0}'', qty={1}, price={2}")
    @CsvSource({
            "apple, 1, 1.99",
            "banana, 2, 2.99"
    })
    void test3(String fruit, int qty, BigDecimal price) {
        assertTrue(true);
    }

    /**
     * csv文件内容:
     * name, age
     * shawn, 24
     */
    @DisplayName("参数化测试-从csv文件获取")
    @ParameterizedTest
    // 指定csv文件位置,并忽略标题行
    @CsvFileSource(resources="/test.csv", numLinesToSkip=1)  
    public void parameterizedTestWithCsv(String name, Integer age) {
        System.out.println("name:" + name + ", age:" + age);
        Assertions.assertNotNull(name);
        Assertions.assertNotNull(age);
    }

}

# Json 数据驱动


public class ParameterTest {

    /**
     * Json文件内容:
     * [
     *   {"name": "apple", "age": "12"},
     *   {"name": "banana", "age": "13"}
     * ]
     */
    static List<User> testDDTFromJson() throws IOException {

        ObjectMapper objectMapper = new ObjectMapper();
        TypeReference typeReference = new TypeReference<List<User>>(){};

        List<User> users = (List<User>) objectMapper.readValue(
                ParameterTest.class.getResourceAsStream("/user.json"),  				// 本类名反射
                typeReference
        );

        return users;
    }

    @ParameterizedTest
    // @MethodSource("testDDTFromJson")  // 指定获取数据源的方法名
    @MethodSource  // 若不指定方法名,则自动找同名方法
    @DisplayName("从方法获取测试数据")
     void testDDTFromJson(User user) {
        System.out.println(user);
        Assertions.assertTrue(user.name.length() > 3);
    }

}

# Yaml 数据驱动


public class ParameterTest {

    /**
     * Yaml文件内容:
     * - name: apple
     *   age: 12
     * - name: banana
     *   age: 13
     */
    static List<User> testDDTFromYaml() throws IOException {

        ObjectMapper objectMapper = new ObjectMapper(new YAMLFactory());
        TypeReference typeReference = new TypeReference<List<User>>(){};
		
        List<User> users = (List<User>) objectMapper.readValue(
                ParameterTest.class.getResourceAsStream("/user.yaml"),  // 本类名反射
                typeReference
        );
		
        return users;
    }

    @ParameterizedTest
    // @MethodSource("testDDTFromYaml")  // 指定获取数据源的方法名
    @MethodSource  // 若不指定方法名,则自动找同名方法
    @DisplayName("从方法获取测试数据")
     void testDDTFromYaml(User user) {
        System.out.println(user);
        Assertions.assertTrue(user.name.length() > 3);
    }

}

# 2) 嵌套测试


class NestedTest {

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

    @Test
    @DisplayName("Nested")
    void test() {
        System.out.println("test");
    }

    @Nested
    @DisplayName("Nested2")
    class Nested2 {

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

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

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

        @Nested
        @DisplayName("Nested3")
        class Nested3 {

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

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

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

}

# 3) 重复测试

@RepeatedTest(10)  // 表示重复执行10次
@DisplayName("重复测试")
public void testRepeated() {
    Assertions.assertTrue(1==1);
}

# 4) 前置条件

JUnit5 中的前置条件(Assumptions)类似于断言,不同之处在于不满足的断言会使得测试方法失败,而不满足的前置条件只会使得测试方法的执行终止。

前置条件可以看成是测试方法执行的前提,当该前提不满足时,就没有继续执行的必要。在如下案例中:

  • assumeTrue 和 assumFalse 确保给定的条件为 true 或 false,不满足条件会使得测试方法执行终止。
  • assumingThat 的参数是分别表示条件的布尔值和 Executable 接口的实现对象。只有条件满足时,Executable 对象才会被执行;当条件不满足时,测试方法的执行并不会终止。

public class AssumeTest {

    private final String env = "dev1";

    @Test
    @DisplayName("assume simple")
    public void testSimpleAssume() {
        assumeTrue(Objects.equals(this.env, "dev1"));
        System.out.println("环境是 dev1");  // 执行输出
        assumeTrue(Objects.equals(this.env, "dev2"));  // org.opentest4j.TestAbortedException: Assumption failed: assumption is not true
        System.out.println("环境是 dev2");  // 不执行输出
    }

    @Test
    @DisplayName("assume then do")
    public void testAssumeThenDo() {
        assumingThat(  // 只有条件满足时,Executable 对象才会被执行
                Objects.equals(this.env, "dev2"),  // 表示条件的布尔值
                () -> System.out.println("In dev1")  // Executable 接口的实现对象
        );
        // 即使上述不满足条件,也会继续执行剩下代码
        System.out.println("始终执行");
    }

}

# 5) 测试执行顺序

# Alphanumeric:按字母数字顺序

/**
Alphanumeric:按字母数字顺序
*/
@TestMethodOrder(MethodOrderer.Alphanumeric.class)
public class AlphanumericTest {

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

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

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

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

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

}

# OrderAnnotation:根据 @Order 值排序

 
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class OrderAnnotationrTest {
 
    @Test
    void test0() {
        assertEquals(2, 1 + 1);
    }
 
    @Test
    @Order(3)
    void test1() {
        assertEquals(2, 1 + 1);
    }
 
    @Test
    @Order(1)
    void test2() {
        assertEquals(2, 1 + 1);
    }
 
    @Test
    @Order(2)
    void test3() {
        assertEquals(2, 1 + 1);
    }
 
    @Test
    void test4() {
        assertEquals(2, 1 + 1);
    }
 
}

# Random:随机顺序

@TestMethodOrder(MethodOrderer.Random.class)
class RandomTest {
 
    @Test
    void aTest() {}
 
    @Test
    void bTest() {}
 
    @Test
    void cTest() {}
}

# 2. JUnit5 新特性

  • JUnit5 不再是单个库,而是模块化结构的集合。整个 API 分成了:自己的模块、引擎、Launcher、针对 Gradle 和 Surefire 的集成模块。

  • 更强大的断言功能和测试注解。

  • 嵌套测试类:不仅仅是 BDD(Behavior Driven Development)。

  • 动态测试:在运行时生成测试用例

  • 扩展测试:JUnit5 提供了很多的标准扩展接口,第三方可以直接实现这些接口来提供自定义的行为。通过 @ExtendWith 注解可以声明在测试方法和类的执行中启用相应的扩展。

  • 支持 Hamcrest 匹配和 AssertJ 断言库,可以用它们来代替 JUnit5 的方法

  • 实现了模块化,让测试执行和测试发现等不同模块解耦,减少依赖。

  • 提供对 Java8 的支持,如 Sream API、Lambda 表达式(允许你通过表达式来代替功能接口)等。

  • 提供了分组断言(允许执行一组断言,且会一起报告)。

  • JUnit5 不再需要手动将测试类与测试方法为 public,包可见的访问级别就足够了。

  • 因为框架会为每个测试类创建一个单独的实例,且在 @BeforeAll/@AfterAll 方法执行时,尚无任何测试实例诞生。因此,这两个方法必须定义为静态方法。

# 3. 断言 Assertions

JUnit5 使用了新的断言类:org.junit.jupiter.api.Assertions。相比之前的 Assert 断言类多了许多新的功能,并且大量方法支持 Java8 的 Lambda 表达式。

方法 说明
assertEquals(expected, actual) 查看两个对象是否相等。(类似于字符串比较使用的 equals() 方法
assertNotEquals(first, second) 查看两个对象是否不相等。
assertNull(object) 查看对象是否为空。
assertNotNull(object) 查看对象是否不为空。
assertSame(expected, actual) 查看两个对象的引用是否相等。(类似于使用“==”比较两个对象)
assertNotSame(unexpected, actual) 查看两个对象的引用是否不相等。(类似于使用“!=”比较两个对象)
assertTrue(condition) 查看运行结果是否为 true。
assertFalse(condition) 查看运行结果是否为 false。
assertArrayEquals(expecteds, actuals) 查看两个数组是否相等。
assertThat(actual, matcher) 查看实际值是否满足指定的条件。
fail() 让测试执行失败。
assertThrows() 异常断言
assertTimeout() 方法 超时断言
Last Updated: 12/15/2023, 8:18:50 AM