# 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() 方法 | 超时断言 |