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.*;@DisplayName("前置条件测试") public class AssumptionsTest { private final String environment = "DEV" ; @Test @DisplayName("前置条件断言校验") void simpleAssume () { assumeTrue(Objects.equals(this .environment, "DEV" )); assumeFalse(() -> Objects.equals(this .environment, "BETA" )); } @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" )); } }
执行结果:
基础测试 根据提供的测试用例生命周期来初体验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.*;@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("第二个测试用例..." ); } }
执行结果:
可以看到左边一栏的结果里显示测试项名称就是我们在测试类和方法上使用 @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(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) { } }
执行结果:
除了内建的两种别名生成方法,我们也可以在这两个方法基础上扩展生成自己的别名生成方法。如下是一个官方的示例,可以将测试类和测试方法名称组合并替换下划线,并在类别名后自动添加“…”。
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(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; } } }
执行结果
重复测试 在 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;@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("执行测试..." ); } }
执行结果:
禁止测试 在运行测试类时,需要跳过某个测试方法,正常运行其他的测试用例时,可以使用注解@Disabled,表明该测试方法不可用,在执行测试类时就不会被执行。
1 2 3 4 5 6 @Test @DisplayName("第三个测试用例") @Disabled void testNumberThree () { System.out.println("第三个测试用例,我需要禁止..." ); }
执行结果:
运行后可以看到,@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.*;@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)); } } } } }
执行结果:
分组测试 在测试方法比较多的时,我们可以给测试方法打上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;@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..." ); } }
执行结果:
从执行结果看出,是按数字的顺序和字母表排序且数字在字母之前。
指定的数值,添加注解@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.*;@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..." ); } }
执行结果
随机,对测试方法进行伪随机排序
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;@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..." ); } }
第一次执行,执行结果
第二次执行,执行结果
自定义测试顺序
参数化测试 在我们编写的测试用例时,需要传不同的参数进行不同场景的测试,但是不需要修改测试的方法,只需要改变参数即可,这时使用的测试就是参数化测试方式。
要使用 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;@DisplayName("参数化测试") public class ParameterizedTestCase { @ParameterizedTest @ValueSource(strings = {"red", "green", "yellow"}) @DisplayName("String参数源参数化测试") void testValueSourceForString (String value) { System.out.println(value); } @ParameterizedTest @ValueSource(ints = {2, 4, 8}) @DisplayName("Int参数源参数化测试") void testValueSourceForInt (int num) { System.out.println(num); } @ParameterizedTest @ValueSource(doubles = {2.D, 4.D, 8.D}) @DisplayName("double参数源参数化测试") void testValueSourceForDouble (double num) { System.out.println(num); } @ParameterizedTest @ValueSource(longs = {2L, 4L, 8L}) @DisplayName("long参数源参数化测试") void testValueSourceForLong (long num) { System.out.println(num); } @ParameterizedTest(name = "[{index}]TimeUnit:{arguments}") @EnumSource(value = TimeUnit.class) @DisplayName("枚举类参数源参数化测试") void testForEnumSourceAll (TimeUnit timeUnit) { System.out.println(timeUnit.toString()); } @ParameterizedTest @EnumSource(value = TimeUnit.class, names = {"DAYS", "HOURS"}) @DisplayName("枚举类参数源指定枚举值参数化测试") void testForEnumSourceInclude (TimeUnit timeUnit) { System.out.println(timeUnit.toString()); } @ParameterizedTest @EnumSource(value = TimeUnit.class, mode = EnumSource.Mode.EXCLUDE, names = {"DAYS", "HOURS"}) @DisplayName("枚举类参数源不包含枚举值参数化测试") void testForEnumSourceExclude (TimeUnit timeUnit) { System.out.println(timeUnit.toString()); } @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" ); } @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" )) ); } @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)); } @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); } @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); } } @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;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 ) ); } }
执行结果:
异常验证测试 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.*;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 () { Throwable exception = assertThrows(ArithmeticException.class, () -> { 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 ); }); } } }
执行结果:
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.*;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 () { Throwable exception = assertThrows(ArithmeticException.class, () -> { 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 ), () -> { }); } @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" ; } }
执行结果:
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.*;@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" ); } }
执行结果
并行测试 默认情况下,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;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
开启并行模式 在资源目录下编写配置文件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;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线程