Junit5用例编写

创建Maven工程,pom.xml引入Junit5的依赖坐标,注意:JDK环境必须在1.8以上

1
2
3
4
5
6
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>RELEASE</version>
<scope>test</scope>
</dependency>

Junit5的依赖坐标可以单独导入junit-jupiter-api、junit-jupiter-engine、junit-jupiter-params,如若单独引入较为麻烦,可以引入如下依赖坐标,包含了这个三个单独的依赖:

1
2
3
4
5
6
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>RELEASE</version>
<scope>test</scope>
</dependency>

前置条件

前置条件(assumptions)类似于断言,不同之处在于不满足的断言会使得测试方法失败,而不满足的前置条件只会使得测试方法的执行终止。前置条件可以看成是测试方法执行的前提,当该前提不满足时,就没有继续执行的必要。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package com.test;

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

import java.util.Objects;

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

/**
* @author jingLv
* @date 2020/10/22
*/
@DisplayName("前置条件测试")
public class AssumptionsTest {

private final String environment = "DEV";

/**
* assumeTrue 和 assumFalse 确保给定的条件为 true 或 false,不满足条件会使得测试执行终止
*/
@Test
@DisplayName("前置条件断言校验")
void simpleAssume() {
assumeTrue(Objects.equals(this.environment, "DEV"));
assumeFalse(() -> Objects.equals(this.environment, "BETA"));
}

/**
* assumingThat 的参数是表示条件的布尔值和对应的 Executable 接口的实现对象。只有条件满足时,Executable 对象才会被执行;当条件不满足时,测试执行并不会终止。
*/
@Test
@DisplayName("前置条件断言成功后执行")
void assumeThenDo() {
assumingThat(Objects.equals(this.environment, "DEV"), () -> System.out.println("In dev"));
}

@Test
@DisplayName("前置条件断言失败后终止执行")
void assumeThenFail() {
assumingThat(Objects.equals(this.environment, "BETA"), () -> System.out.println("In dev"));
}
}

执行结果:

image-20201022103736249

基础测试

根据提供的测试用例生命周期来初体验Junit5

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
package com.test;

import org.junit.jupiter.api.*;

/**
* @author jingLv
* @date 2020/10/21
*/
@DisplayName("初体验Junit5")
class FirstTestCase {

@BeforeAll
@DisplayName("初始化")
static void init() {
System.out.println("初始化...");
}

@AfterAll
@DisplayName("已结束")
static void clean() {
System.out.println("已结束...");
}

@BeforeEach
@DisplayName("测试方法开始")
void tearUp() {
System.out.println("测试方法开始...");
}

@AfterEach
@DisplayName("测试方法结束")
void tearDown() {
System.out.println("测试方法结束...");
}

@Test
@DisplayName("第一个测试用例")
void testNumberOne() {
System.out.println("第一个测试用例...");
}

@Test
@DisplayName("第二个测试用例")
void testNumberTwo() {
System.out.println("第二个测试用例...");
}
}

执行结果:

image-20201021135753820

可以看到左边一栏的结果里显示测试项名称就是我们在测试类和方法上使用 @DisplayName 设置的名称,这个注解就是 JUnit 5 引入,用来定义一个测试类并指定用例在测试报告中的展示名称,这个注解可以使用在类上和方法上,在类上使用它就表示该类为测试类,在方法上使用则表示该方法为测试方法。

@BeforeAll **和 **@AfterAll **,它们定义了整个测试类在开始前以及结束时的操作,只能修饰静态方法,主要用于在测试过程**中所需要的全局数据和外部资源的初始化和清理。

@BeforeEach@AfterEach 所标注的方法会在每个测试用例方法开始前和结束时执行,主要是负责该测试用例所需要的运行环境的准备和销毁。

设置用例别名

上面已经介绍了@DisplayName的使用,可以注解到类和方法上设置显示的名称,除了该注解,还提供了DisplayNameGenerator扩展类来根据类或方法名进行制定,生成个性化的别名。DisplayNameGenerator 支持两类扩展扩展:

  • Standard 即当前使用的默认规则
  • ReplaceUnderscores 用于替换下划线的内建方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package com.test;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.DisplayNameGeneration;
import org.junit.jupiter.api.DisplayNameGenerator;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

/**
* 使用@DisplayNameGeneration()设置别名个性化测试
*
* @author jingLv
* @date 2020/07/31
*/
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
public class A_year_is_not_supported {

@Test
void if_it_is_zero() {
}

@DisplayName("闰年计算不支持负数")
@ParameterizedTest(name = "For example, year {0} is not supported.")
@ValueSource(ints = {-1, -4})
void if_it_is_negative(int year) {
}
}

执行结果:

image-20201022145222670

除了内建的两种别名生成方法,我们也可以在这两个方法基础上扩展生成自己的别名生成方法。如下是一个官方的示例,可以将测试类和测试方法名称组合并替换下划线,并在类别名后自动添加“…”。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
package com.test;

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

import java.lang.reflect.Method;

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

/**
* 使用@DisplayNameGeneration()设置别名个性化测试
*
* @author jingLv
* @date 2020/07/31
*/
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
public class A_year_is_not_supported {

@Test
void if_it_is_zero() {
}

@DisplayName("闰年计算不支持负数")
@ParameterizedTest(name = "For example, year {0} is not supported.")
@ValueSource(ints = {-1, -4})
void if_it_is_negative(int year) {
}

@Nested
@DisplayNameGeneration(IndicativeSentences.class)
class A_year_is_a_leap_year {

@Test
void if_it_is_divisible_by_4_but_not_by_100() {
}

@ParameterizedTest(name = " {0} 年是闰年.")
@ValueSource(ints = {2016, 2020, 2048})
void if_it_is_one_of_the_following_years(int year) {
}

}

static class IndicativeSentences extends DisplayNameGenerator.ReplaceUnderscores {

@Override
public String generateDisplayNameForClass(Class<?> testClass) {
return super.generateDisplayNameForClass(testClass);
}

@Override
public String generateDisplayNameForNestedClass(Class<?> nestedClass) {
return super.generateDisplayNameForNestedClass(nestedClass) + "...";
}

@Override
public String generateDisplayNameForMethod(Class<?> testClass, Method testMethod) {
String name = testClass.getSimpleName() + ' ' + testMethod.getName();
return name.replace('_', ' ') + '.';
}
}

/**
* 以下是在官方示例基础上扩展出的另一个别名生成方法,来支持使用驼峰命名法的用例,将驼峰命名的用例方法用空格进行分隔。
*/
@Nested
@DisplayNameGeneration(ReplaceCamelCase.class)
class ThisIsACamelTestCase {

@Test
void TodayIsHistory() {
assertTrue(true);
}

@Test
void TodayWillBeRemembered() {
assertTrue(true);
}

}

static class ReplaceCamelCase extends DisplayNameGenerator.Standard {
public ReplaceCamelCase() {
}

public String generateDisplayNameForClass(Class<?> testClass) {
return this.replaceCapitals(super.generateDisplayNameForClass(testClass));
}

public String generateDisplayNameForNestedClass(Class<?> nestedClass) {
return this.replaceCapitals(super.generateDisplayNameForNestedClass(nestedClass));
}

public String generateDisplayNameForMethod(Class<?> testClass, Method testMethod) {
return this.replaceCapitals(testMethod.getName());
}

private String replaceCapitals(String name) {
name = name.replaceAll("([A-Z])", " $1");
name = name.replaceAll("([0-9].)", " $1");
return name;
}
}
}

执行结果

image-20201022145646433

重复测试

在 JUnit 5 里新增了对测试方法设置运行次数的支持,允许让测试方法进行重复运行。当要运行一个测试方法 N次时,可以使用 @RepeatedTest 标记它。

重复运行的测试方法名称进行修改,利用 @RepeatedTest 提供的内置变量,以占位符方式在其 name 属性上使用。

@RepeatedTest 注解中还可以携带一些内置参数,在方法别名中显示对应的方法信息。

  • **{displayName}**:方法的别名
  • **{currentRepetition}**:当前执行次数
  • **{totalRepetitions}**:总执行次数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package com.test;

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

/**
* @author jingLv
* @date 2020/10/22
*/
@DisplayName("重复测试")
public class RepeatedTestCase {

@RepeatedTest(3)
@DisplayName("重复测试三次")
void repeatedTestOne() {
System.out.println("执行测试...");
}

@RepeatedTest(value = 3, name = "{displayName}第{currentRepetition}次,一共执行了{totalRepetitions}次")
@DisplayName("自定义名称重复测试")
void repeatedTestTwo() {
System.out.println("执行测试...");
}
}

执行结果:

image-20201022132228671

禁止测试

在运行测试类时,需要跳过某个测试方法,正常运行其他的测试用例时,可以使用注解@Disabled,表明该测试方法不可用,在执行测试类时就不会被执行。

1
2
3
4
5
6
@Test
@DisplayName("第三个测试用例")
@Disabled
void testNumberThree() {
System.out.println("第三个测试用例,我需要禁止...");
}

执行结果:

image-20201021140556597

运行后可以看到,@Disabled标记的方法不会执行,只有单独的方法信息打印

@Disabled 也可以使用在类上,用于标记类下所有的测试方法不被执行,一般使用对多个测试类组合测试的时候。

内嵌测试

当我们编写的类和代码逐渐增多,随之而来的需要测试的对应测试类也会越来越多。为了解决测试类数量爆炸的问题,JUnit 5提供了@Nested 注解,能够以静态内部成员类的形式对测试用例类进行逻辑分组。 并且每个静态内部类都可以有自己的生命周期方法, 这些方法将按从外到内层次顺序执行。 此外,嵌套的类也可以用@DisplayName 标记,这样我们就可以使用正确的测试名称。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
package com.test;

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.HashMap;
import java.util.Map;

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

/**
* 测试HashMap功能
*
* @author jingLv
* @date 2020/10/21
*/
@DisplayName("内嵌测试类")
class NestUnitTest {

Map<String, Object> map;

@Nested
@DisplayName("新建Map")
class GivenCreateNewMap {
@BeforeEach
void create() {
map = new HashMap<>();
}

@Test
@DisplayName("断言Map是否为空")
void isEmpty() {
assertTrue(map.isEmpty());
}

@Nested
@DisplayName("添加元素到Map")
class ThenAddMap {
String key = "xiaohong";
Object value = "123123";

@BeforeEach
void add() {
map.put(key, value);
}

@Test
@DisplayName("断言Map是否为空")
void isEmpty() {
assertFalse(map.isEmpty());
}

@Test
@DisplayName("断言value得值是否是设置的值")
void verifyValue() {
assertEquals(value, map.get(key));
}

@Nested
@DisplayName("删除Map中元素")
class RemoveMap {
@BeforeEach
void remove() {
map.remove(key);
}

@Test
@DisplayName("断言Map是否为空")
void isEmpty() {
assertTrue(map.isEmpty());
}

@Test
@DisplayName("断言Map是否为空")
void verifyValue() {
assertNull(map.get(key));
}
}
}
}

}

执行结果:

image-20201022111519005

分组测试

在测试方法比较多的时,我们可以给测试方法打上Tag标签,通过Tag标签进行分类,根据分类可进行针对性的测试。

@Tag 也可以使用在类上,用于对类标记进行分组。

指定顺序测试

测试方法执行的顺序默认根据Junit5一种确定但不明显的算法进行的,在测试方法执行时需要指定测试方法的执行顺序。Junit5是通过MethodOrderer类控制测试方法的执行

  • 字母数字,按照测试方法名字母数字的顺序指定测试顺序

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    package com.test;

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

    /**
    * @author jingLv
    * @date 2020/10/21
    */
    @TestMethodOrder(MethodOrderer.Alphanumeric.class)
    @DisplayName("按照测试方法名的字母数字排序")
    public class MethodAlphanumericTest {

    @Test
    @DisplayName("我是Z")
    void testZ() {
    System.out.println("我是Z...");
    }

    @Test
    @DisplayName("我是1")
    void test1() {
    System.out.println("我是1...");
    }

    @Test
    @DisplayName("我是A")
    void testA() {
    System.out.println("我是A...");
    }

    @Test
    @DisplayName("我是G")
    void testG() {
    System.out.println("我是G...");
    }

    @Test
    @DisplayName("我是3")
    void test3() {
    System.out.println("我是3...");
    }
    }

    执行结果:

    image-20201021150308949

    从执行结果看出,是按数字的顺序和字母表排序且数字在字母之前。

  • 指定的数值,添加注解@Order的值指定测试顺序,数字小的先执行

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    package com.test;

    import org.junit.jupiter.api.*;

    /**
    * @author jingLv
    * @date 2020/10/21
    */
    @TestMethodOrder(MethodOrderer.OrderAnnotation.class)
    @DisplayName("测试方法指定顺序测试")
    public class MethodOrderTest {

    @Test
    @Order(3)
    @DisplayName("我是1")
    void test1() {
    System.out.println("我是1...");
    }

    @Test
    @Order(2)
    @DisplayName("我是2")
    void test2() {
    System.out.println("我是2...");
    }

    @Test
    @Order(1)
    @DisplayName("我是3")
    void test3() {
    System.out.println("我是3...");
    }
    }

    执行结果

    image-20201021150642169

  • 随机,对测试方法进行伪随机排序

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    package com.test;

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

    /**
    * @author jingLv
    * @date 2020/10/21
    */
    @DisplayName("测试方法随机测试")
    @TestMethodOrder(MethodOrderer.Random.class)
    public class MethodRandomTest {
    @Test
    @DisplayName("我是Z")
    void testZ() {
    System.out.println("我是Z...");
    }

    @Test
    @DisplayName("我是1")
    void test1() {
    System.out.println("我是1...");
    }

    @Test
    @DisplayName("我是A")
    void testA() {
    System.out.println("我是A...");
    }

    @Test
    @DisplayName("我是G")
    void testG() {
    System.out.println("我是G...");
    }

    @Test
    @DisplayName("我是3")
    void test3() {
    System.out.println("我是3...");
    }
    }

    第一次执行,执行结果

    image-20201021151146605

    第二次执行,执行结果

    image-20201021151329621

  • 自定义测试顺序

    • 实现MethodOrderer创建自定义测试顺序

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      package com.test;

      import org.junit.jupiter.api.MethodDescriptor;
      import org.junit.jupiter.api.MethodOrderer;
      import org.junit.jupiter.api.MethodOrdererContext;

      import java.util.Comparator;

      /**
      * 自定义排序方式:根据参数计数的顺序
      *
      * @author jingLv
      * @date 2020/10/28
      */
      public class ParameterCountOrder implements MethodOrderer {

      private final Comparator<MethodDescriptor> comparator = Comparator.comparingInt(md -> md.getMethod().getParameterCount());

      @Override
      public void orderMethods(MethodOrdererContext methodOrdererContext) {
      methodOrdererContext.getMethodDescriptors().sort(comparator.reversed());
      }
      }

    • @ParameterizedTest测试上述自定义排序

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      package com.test;

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

      import java.math.BigDecimal;

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

      /**
      * @author jingLv
      * @date 2020/10/28
      */
      @TestMethodOrder(ParameterCountOrder.class)
      public class MethodParameterCountTest {

      @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 : 1")
      @ParameterizedTest(name = "{index} ==> ints={0}")
      @ValueSource(ints = {1, 2, 3})
      void test1(int num1) {
      assertTrue(num1 < 4);
      }

      @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);
      }

      }

      执行结果:

      image-20201028144914429

参数化测试

在我们编写的测试用例时,需要传不同的参数进行不同场景的测试,但是不需要修改测试的方法,只需要改变参数即可,这时使用的测试就是参数化测试方式。

要使用 JUnit 5 进行参数化测试,除了 junit-jupiter-engine 基础依赖之外,还需要另个模块依赖:junit-jupiter-params,其主要就是提供了编写参数化测试 API。Maven工程pom.xml引入如下坐标:

1
2
3
4
5
6
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<version>RELEASE</version>
<scope>test</scope>
</dependency>

支持的参数化方式

注解 类型 说明
@ValueSource 基本数据源 Java 的八大基本类型和字符串,Class,使用时赋值给注解上对应类型属性,以数组方式传递
@EnumSource 枚举类型数据源 给指定 Enum 枚举类型传入,构造出枚举类型中特定的值
@MethodSource 方法数据源 指定一个返回的 Stream / Array / 可迭代对象 的方法作为数据源。 需要注意的是该方法必须是静态的,并且不能接受任何参数。
@CsvSource CSV数据源 注入指定 CSV 格式 (comma-separated-values) 的一组数据,用每个逗号分隔的值来匹配一个测试方法对应的参数
@CsvFileSource CSV文件数据源 注入指定的CSV文件
@ArgumentsSource 过自定义的参数数据源 通过实现 ArgumentsProvider 接口的参数类来作为数据源,重写它的 provideArguments 方法可以返回自定义类型的Stream<Arguments>,作为测试方法所需要的数据使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
package com.test;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.*;

import java.util.Arrays;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.stream.Stream;

import static org.junit.jupiter.params.provider.Arguments.arguments;

/**
* 参数测试
* <p>
* - @ParameterizedTest 很实用的注解,需要junit-jupiter-params依赖
*
* @author jingLv
* @date 2020/07/31
*/
@DisplayName("参数化测试")
public class ParameterizedTestCase {

/**
* String参数源
*
* @param value 参数值
*/
@ParameterizedTest
@ValueSource(strings = {"red", "green", "yellow"})
@DisplayName("String参数源参数化测试")
void testValueSourceForString(String value) {
System.out.println(value);
}

/**
* int参数源
*
* @param num 参数值
*/
@ParameterizedTest
@ValueSource(ints = {2, 4, 8})
@DisplayName("Int参数源参数化测试")
void testValueSourceForInt(int num) {
System.out.println(num);
}

/**
* double参数源
*
* @param num 参数值
*/
@ParameterizedTest
@ValueSource(doubles = {2.D, 4.D, 8.D})
@DisplayName("double参数源参数化测试")
void testValueSourceForDouble(double num) {
System.out.println(num);
}

/**
* Long参数源
*
* @param num 参数值
*/
@ParameterizedTest
@ValueSource(longs = {2L, 4L, 8L})
@DisplayName("long参数源参数化测试")
void testValueSourceForLong(long num) {
System.out.println(num);
}

/**
* 枚举类参数化测试
*
* @param timeUnit jdk自动的time枚举类
*/
@ParameterizedTest(name = "[{index}]TimeUnit:{arguments}")
@EnumSource(value = TimeUnit.class)
@DisplayName("枚举类参数源参数化测试")
void testForEnumSourceAll(TimeUnit timeUnit) {
System.out.println(timeUnit.toString());
}

/**
* 注入枚举类,选择部分
*
* @param timeUnit jdk自动的time枚举类
*/
@ParameterizedTest
@EnumSource(value = TimeUnit.class, names = {"DAYS", "HOURS"})
@DisplayName("枚举类参数源指定枚举值参数化测试")
void testForEnumSourceInclude(TimeUnit timeUnit) {
System.out.println(timeUnit.toString());
}

/**
* 注入枚举类,不包含
*
* @param timeUnit jdk自动的time枚举类
*/
@ParameterizedTest
@EnumSource(value = TimeUnit.class, mode = EnumSource.Mode.EXCLUDE, names = {"DAYS", "HOURS"})
@DisplayName("枚举类参数源不包含枚举值参数化测试")
void testForEnumSourceExclude(TimeUnit timeUnit) {
System.out.println(timeUnit.toString());
}

/**
* 注入枚举类,正则匹配
*
* @param timeUnit jdk自动的time枚举类
*/
@ParameterizedTest
@EnumSource(value = TimeUnit.class, mode = EnumSource.Mode.MATCH_ALL, names = ".*SECONDS")
@DisplayName("枚举类参数源正则匹配枚举值参数化测试")
void testForEnumSourceMatch(TimeUnit timeUnit) {
System.out.println(timeUnit.toString());
}


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

/**
* 通过方法返回值,单个参数
*
* @param argument 方法中的参数
*/
@ParameterizedTest
@MethodSource("stringProvider")
@DisplayName("方法返回值参数源单个参数")
void testWithExplicitLocalMethodSource(String argument) {
System.out.println(argument);
}

static Stream<Arguments> stringIntAndListProvider() {
return Stream.of(
arguments("apple", 1, Arrays.asList("a", "b")),
arguments("lemon", 2, Arrays.asList("x", "y"))
);
}

/**
* 通过方法返回值参数元,多个参数
*
* @param str 参数列表第一个参数:string
* @param num 参数列表第二个参数:int
* @param list 参数列表第三个参数:list
*/
@ParameterizedTest(name = "[{index} fruits name:{0} and number:{1} and list: {2}]")
@MethodSource("stringIntAndListProvider")
@DisplayName("方法返回值参数源多参数类型")
void testWithMultiArgMethodSource(String str, int num, List<String> list) {
// 多参支持
System.out.printf("Content: %s is %d, %s%n", str, num, String.join(",", list));
}


/**
* csv参数源,根据csv格式的形式,以逗号分隔
*
* @param fruit 水果名称
* @param rank 水果等级
*/
@ParameterizedTest(name = "[{index} fruits name:{0} and rank:{1}]")
@CsvSource({
"apple, 1",
"banana, 2",
"'lemon, lime', 0xF1"
})
@DisplayName("csv参数源参数化测试")
void testWithCsvSource(String fruit, int rank) {
System.out.println(fruit + ":" + rank);
}

/**
* csv文件参数源,文件添加到resources下,注意:与测试类一致的相同路径
*
* @param fruit 水果名称
* @param price 水果价格
*/
@ParameterizedTest(name = "[{index} fruits name:{0} and price:{1}]")
@CsvFileSource(resources = "csv/fruit.csv")
@DisplayName("csv文件参数源参数化测试")
void testWithCsvFileSource(String fruit, int price) {
System.out.println(fruit + ":" + price);
}

static class MyArgumentsProvider implements ArgumentsProvider {
@Override
public Stream<? extends Arguments> provideArguments(ExtensionContext context) {
return Stream.of("apple", "banana", "lemon").map(Arguments::of);
}
}

/**
* 通过参数类参数源, 这里引用的类必须实现 ArgumentsProvider 接口
*
* @param argument 参数值
*/
@ParameterizedTest
@ArgumentsSource(MyArgumentsProvider.class)
void testWithArgumentsSource(String argument) {
System.out.println(argument);
}

}

动态测试

JUnit 5 测试方法的创建都是静态的,在编译时刻就已经存在。JUnit 5 新增了对动态测试的支持,可以在运行时动态创建测试并执行。通过动态测试,可以满足一些静态测试无法解的需求,也可以完成一些重复性很高的测试。比如,有些测试用例可能依赖运行时的变量,有时候会需要生成上百个不同的测试用例。这些场景都是动态测试可以发挥其长处的地方。

动态测试是通过新的@TestFactory 注解来实现的。测试类中的方法可以添加@TestFactory 注解的方法来声明其是创建动态测试的工厂方法。这样的工厂方法需要返回 org.junit.jupiter.api.DynamicTest 类的集合,可以是 Stream、Collection、Iterable 或 Iterator 对象。每个表示动态测试的 DynamicTest 对象由显示名称和对应的 Executable 接口的实现对象来组成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package com.test;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.DynamicTest;
import org.junit.jupiter.api.TestFactory;

import java.util.Collection;
import java.util.Collections;
import java.util.stream.Stream;

import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.DynamicTest.dynamicTest;
import static org.junit.jupiter.api.DynamicTest.stream;

/**
* 动态测试用例
*
* @author jingLv
* @date 2020/07/03
*/
public class DynamicTestCase {

@TestFactory
@DisplayName("基础动态测试")
Collection<DynamicTest> simpleDynamicTest() {
return Collections.singleton(dynamicTest("simple dynamic test", () -> assertTrue(true)));
}

@TestFactory
@DisplayName("DynamicTest 提供了一个静态方法 stream 来根据输入生成动态测试")
public Stream<DynamicTest> streamDynamicTest() {
return stream(
Stream.of("Hello", "World").iterator(),
(word) -> String.format("Test - %s", word),
(word) -> assertTrue(word.length() > 4)
);
}
}

执行结果:

image-20201028145628429

异常验证测试

assertThrows

assertThrows 和 assertDoesNotThrow 是 JUnit 5 版本新添加的针对异常的验证方法,弥补了之前版本不能对方法异常直接进行验证的不足。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
package com.test;

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

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

/**
* @author jingLv
* @date 2020/10/22
*/
public class ThrowTestCase {

public static void division(int a, int b) {
int result = 0;
try {
result = a / b;
} catch (ArithmeticException e) {
throw new ArithmeticException("除数不能为0");
}
}

@Nested
@DisplayName("异常校验测试")
class TestAssertThrows {
@Test
@DisplayName("验证抛出除0异常")
void testAssertThrow() {
//验证异常匹配并返回exception对象
Throwable exception = assertThrows(ArithmeticException.class, () -> {
//抛出除0异常
division(5, 0);
});

//校验具体的异常信息
assertEquals("除数不能为0", exception.getMessage());
}

@Test
@DisplayName("验证未抛出除0异常 - 5/0")
void testAssertDoesNotThrowFail() {
//抛出异常,断言失败
assertDoesNotThrow(() -> {
division(5, 0);
});
}

@Test
@DisplayName("验证未抛出除0异常 - 4/2")
void testAssertDoesNotThrowSuccess() {
//未抛出异常,断言成功
assertDoesNotThrow(() -> {
division(4, 2);
});
}
}
}

执行结果:

image-20201022150634872

assertTimeout

AssertTimeout 和 assertTimeoutPreemptively 是 JUnit 5 新增的用于验证测试方法是否执行超时的断言。AssertTimeout 和 assertTimeoutPreemptively 的不同在于,assertTimeoutPreemptively 会在验证的预期超时时间到达后,立即完成断言的返回,而 AssertTimeout 会在验证的方法执行完成后再比较二者时间进行返回。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
package com.test;

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

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

/**
* @author jingLv
* @date 2020/10/22
*/
public class ThrowTestCase {

public static void division(int a, int b) {
try {
int result = a / b;
} catch (ArithmeticException e) {
throw new ArithmeticException("除数不能为0");
}
}

@Nested
@DisplayName("异常校验测试")
class TestAssertThrows {
@Test
@DisplayName("验证抛出除0异常")
void testAssertThrow() {
//验证异常匹配并返回exception对象
Throwable exception = assertThrows(ArithmeticException.class, () -> {
//抛出除0异常
division(5, 0);
});

//校验具体的异常信息
assertEquals("除数不能为0", exception.getMessage());
}

@Test
@DisplayName("验证未抛出除0异常 - 5/0")
void testAssertDoesNotThrowFail() {
//抛出异常,断言失败
assertDoesNotThrow(() -> {
division(5, 0);
});
}

@Test
@DisplayName("验证未抛出除0异常 - 4/2")
void testAssertDoesNotThrowSuccess() {
//未抛出异常,断言成功
assertDoesNotThrow(() -> {
division(4, 2);
});
}
}

@Nested
@DisplayName("超时校验测试")
class TestTimeout {
@Test
@DisplayName("验证方法执行未超时")
void timeoutNotExceeded() {
assertTimeout(ofMinutes(2), () -> {
// 执行不超过2分钟的操作
});
}

@Test
@DisplayName("验证方法执行未超时,并验证执行返回结果")
void timeoutNotExceededWithResult() {
String actualResult = assertTimeout(ofMinutes(2), () -> {
return "a result";
});
assertEquals("a result", actualResult);
}

@Test
@DisplayName("验证调用的方法对象执行未超时,并验证方法执行结果")
void timeoutNotExceededWithMethod() {
String whoRU = assertTimeout(ofMinutes(2), ThrowTestCase::throwTest);
assertEquals("Hello, Junit5", whoRU);
}

@Test
@DisplayName("验证方法执行超时,等待方法执行完成")
void timeoutExceeded() {
assertTimeout(ofMillis(10), () -> {
Thread.sleep(1000);
});
}

@Test
@DisplayName("验证方法执行超时,超时立即终止")
void timeoutExceededWithPreemptiveTermination() {
assertTimeoutPreemptively(ofMillis(10), () -> {
Thread.sleep(1000);
});
}
}

private static String throwTest() {
return "Hello, Junit5";
}
}

执行结果:

image-20201022151831709

AssertAll

AssertAll 也是 JUnit 5 中增加的一个非常实用的断言方法。在之前的版本中,如果在一个测试方法中包含有多个断言方法,断言会在出现失败的情况中断,导致并不能完成对后续的断言完成校验(当然也可以通过多个不同的用例变通实现,但代码比较臃肿)。

JUnit 5 版本支持 Lambda 表达式后,引入 AssertAll, 可以非常简洁地将一组断言进行组合,这样测试方法中的多个断言在执行时均会被执行,结果反映到测试报告中。

AssertAll 还可以实现多层的分组验证,测试用例方法的断言策略灵活性大大增强。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
package com.test;

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

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

/**
* @author jingLv
* @date 2020/10/28
*/
@Nested
@DisplayName("分组校验测试")
class AssertAllTestCase {

@Test
@DisplayName("传统验证方式")
void standardAssertions() {
assertEquals(2, 2);
assertEquals(4, 4, "The optional assertion message is now the last parameter.");
assertTrue('a' < 'b', () -> "Assertion messages can be lazily evaluated -- to avoid constructing complex messages unnecessarily.");
}

@Test
@DisplayName("分组批量验证")
void groupedAssertions() {
//分组中的所有断言均会执行,并体现在测试报告中
Person person = new Person().getDefaultPerson();
assertAll("person",
() -> assertEquals("John", person.getFirstName()),
() -> assertEquals("Huang", person.getLastName())
);
}

@Test
@DisplayName("多级分组批量验证-依赖其他断言")
void dependentAssertions() {
//分组验证,分组中的断言互相不产生影响,所有断言都会被验证
Person person = new Person().getDefaultPerson();
assertAll("properties",
() -> {
//分组外先执行失败的断言,会阻塞后续断言的执行,但不影响组外的断言
String firstName = person.getFirstName();
assertNull(firstName);

//仅当同级上一条断言执行成功执行
assertAll("first name",
() -> assertTrue(firstName.startsWith("J")),
() -> assertTrue(firstName.endsWith("n"))
);
},
//多级分组
() -> {
String lastName = person.getLastName();
assertNotNull(lastName);

//仅当同级中上一条断言执行成功执行
assertAll("last name",
() -> assertTrue(lastName.startsWith("D")),
() -> assertTrue(lastName.endsWith("g"))
);
}
);
}
}

class Person {
private String firstName;
private String lastName;

public Person() {
}

public Person(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}

public String getFirstName() {
return firstName;
}

public String getLastName() {
return lastName;
}

public Person getDefaultPerson() {
return new Person("Xiaohong", "Huang");
}
}

执行结果

image-20201028151007666

并行测试

默认情况下,junit测试是在一个线程中串行执行的,从5.3开始支持并行测试。首先需要在配置文件junit-platform.properties中配置如下参数:

1
junit.jupiter.execution.parallel.enabled=true

但是仅仅开启这个参数是不会起效的,测试仍然是按照单个线程去执行的。在测试树上的每个节点是否并发执行是通过执行的模式来决定的。有以下两种可以选择的模式:

  • SAME_THREAD:强制使用同一个线程
  • CONCURRENT:并发执行(除非某个锁资源导致的串行操作)

默认情况下,测试树上的节点使用的是SAME_THREAD执行模式,可以通过修改默认的配置。如下:

1
2
junit.jupiter.execution.parallel.enabled = true
junit.jupiter.execution.parallel.mode.default = concurrent

配置的默认执行模式会作用于测试树上的每个节点,但是有几个需要主要的例外:

  • 首先就是针对于测试声明周期的配置(默认是方法级别的)如果设置为类级别(Lifecycle.PER_CLASS)则必须保证测试类是线程安全的
  • 另外对于设置测试方法的执行顺序(MethodOrderer(Random模式例外))的话,这个与并发测试是互相矛盾的。

在以上这两种情况下,必须明确在测试类或方法上面添加注解**@Execution(CONCURRENT)**,否则也不会按照并发模式进行测试的。

采用以上的模式,所有的测试类和测试方法都是并发来执行的。

当然还可以通过另外一个配置来保证类是并行测试而内部方法是串行测试的,比如设置类之间并行和方法之间串行

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

设置类之间串行而内部方法是并行的

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

以上两个配置并行和串行的参数对应的四种情况如下所示:

在这里插入图片描述

如果junit.jupiter.execution.parallel.mode.classes.default没有明确配置的话,则按照junit.jupiter.execution.parallel.mode.default的值。

编写测试类,验证下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.test;

import org.junit.jupiter.api.Test;

/**
* @author jingLv
* @date 2021/01/06
*/
class ParallelTest {

@Test
void testMethodOne() {
System.out.println(this.getClass() + "-testMethodOne-" + Thread.currentThread().getName());
}

@Test
void testMethodTwo() {
System.out.println(this.getClass() + "-testMethodTwo-" + Thread.currentThread().getName());
}
}

未开启并行模式

执行结果,可以看到只有一个线程 main

image-20210106162157328

开启并行模式

在资源目录下编写配置文件junit-platform.properties,内容如下:

1
2
junit.jupiter.execution.parallel.enabled=true
junit.jupiter.execution.parallel.mode.default=concurrent

并在测试方法上添加注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.test;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.parallel.Execution;
import org.junit.jupiter.api.parallel.ExecutionMode;

/**
* @author jingLv
* @date 2021/01/06
*/
class ParallelTest {

@Test
@Execution(ExecutionMode.CONCURRENT)
void testMethodOne() {
System.out.println(this.getClass() + "-testMethodOne-" + Thread.currentThread().getName());
}

@Test
@Execution(ExecutionMode.CONCURRENT)
void testMethodTwo() {
System.out.println(this.getClass() + "-testMethodTwo-" + Thread.currentThread().getName());
}
}

执行结果,可以看到出现了多个work线程

image-20210106162555074