Spring Boot(八):JUnit5 🧨

「这是我参与11月更文挑战的第4天,活动详情查看:2021最后一次更文挑战」。

1

  1. JUnit 5 变化

Spring Boot 2.2.0 版本开始引入 JUnit 5 作为单元测试默认库

作为最新版本的 JUnit 框架,JUnit5 与之前版本的 JUnit 框架有很多的不同,由三个不同子项目的几个不同模块组成:

JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage

JUnit Platform: JUnit Platform 是在 JVM 上启动测试框架的基础,不仅支持 JUnit 自制的测试引擎,其他猜测是引擎也都可以接入。

JUnit Jupiter: JUnit Jupiter 提供了 JUnit 5 的鑫编程模型,是 JUnit 5 新特性的核心。内部包含了一个测试引擎,用于在 JUnit Platform 上运行。

JUnit Vintage: 由于 JUint 已经发展多年,为了照顾老的项目,JUnit Vintage 提供了兼容 JUnit4.x, Junit3.x 的测试引擎。

注意:

  • Spring Boot 2.4 以上版本移除了默认对 Vintage 的依赖。如果需要兼容 junit4 需要自行引入(不能使用 junit4 的功能 @Test)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
xml复制代码<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<!--移除Vintage依赖,不兼容JUnit4-->
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

1.1 JUnit4 & JUnit5

以前 JUnit4 的使用:

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
java复制代码import entity.Book;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

/**
* @Author Author_Unknown
* @Date 2021/11/10 7:17
* @Description JUnit4使用注解的测试
*/
@RunWith(SpringJUnit4ClassRunner.class) // 指定测试框架的版本
@ContextConfiguration("classpath:bean1.xml")
public class JTest4 {

@Autowired
private Book book;

@Test
public void testBook() {
book.testAttr();
}

}

现在 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
java复制代码import entity.Book;
//import org.junit.Test; // 不能在单一类型的导入中定义(即使用JUnit5就不能同时使用JUnit4)
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;

/**
* @Author Author_Unknown
* @Date 2021/11/10 7:29
* @Description JUnit5的测试
*/
//@ExtendWith(SpringExtension.class) // (1)
//@ContextConfiguration("classpath:bean1.xml") // (2)
@SpringJUnitConfig(locations = "classpath:bean1.xml") // 等价于 (1) + (2)
public class JTest5 {

@Autowired
private Book book;

@Test
public void test() {
System.out.println(book);
}
}
  1. JUnit 5 的常用注解

JUnit 5 的注解与 JUnit 4 的注解有所变化:junit.org/junit5/docs…

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

2.1 @RepeatedTest

1
2
3
4
5
6
7
8
9
java复制代码@SpringBootTest
class SpringBoot_Junit5 {

@Test
@RepeatedTest(3)
void test() {
System.out.println("@RepeatedTest..");
}
}

2.2 @DisplayName

1
2
3
4
5
6
7
8
9
10
11
java复制代码@DisplayName("JUnit5测试类")
@SpringBootTest
class SpringBoot_Junit5 {

@Test
@DisplayName("JUnit5测试方法")
void contextLoads() {
System.out.println("Say something..");
}

}

2.3 @BeforeEach & @AfterEach

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
java复制代码@DisplayName("JUnit5测试类")
@SpringBootTest
class SpringBoot_Junit5 {

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

@AfterEach
public void afterEachMethod() {
System.out.println("@AfterEach..");
}

@Test
@DisplayName("JUnit5测试方法①")
void contextLoads() {
System.out.println("Say something..");
}

@Test
@DisplayName("JUnit5测试方法②")
void test2() {
System.out.println("Say what?");
}

}

2.4 @BeforeAll & @AfterAll

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
java复制代码@DisplayName("JUnit5测试类")
@SpringBootTest
class SpringBoot_Junit5 {

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

@AfterEach
void afterEachMethod() {
System.out.println("@AfterEach..");
}

@BeforeAll
static void beforeAll() {
System.out.println("@BeforeAll..");
System.out.println("============");
}

@AfterAll
static void afterAll() {
System.out.println("============");
System.out.println("@AfterAll..");
}

@Test
@DisplayName("JUnit5测试方法①")
void contextLoads() {
System.out.println("Say something..");
}

@Test
@DisplayName("JUnit5测试方法②")
void test2() {
System.out.println("Say what?");
}

}

2.5 @Disabled

1
2
3
4
5
6
java复制代码@Test
@DisplayName("JUnit5测试方法②")
@Disabled
void test2() {
System.out.println("Say what?");
}

2.6 @Timeout

@Timeout 规定超时时间,超过指定时间后会抛出异常。

1
2
3
4
5
6
7
java复制代码@Test
@DisplayName("JUnit5测试方法①")
@Timeout(value = 500, unit = TimeUnit.MILLISECONDS)
void contextLoads() throws InterruptedException {
Thread.sleep(550);
System.out.println("Say something..");
}

  1. 断言机制

断言(Assertions)是测试方法中的核心部分,用来对测试需要满足的条件进行验证。这些断言方法都是 org.junit.jupiter.api.Assertions 的静态方法

JUnit 5 内置的断言可以分成如下几个类别:

  • 检查业务逻辑返回的数据是否合理
  • 所有的测试运行结束以后,会有一个详细的测试报告

3.1 简单断言

用来对单个值进行简单的验证,如:

方法 说明
assertEquals 判断两个对象或两个原始类型是否相等
assertNotEquals 判断两个对象或两个原始类型是否不相等
assertSame 判断两个对象引用是否指向同一个对象
assertNotSame 判断两个对象引用是否指向不同的对象
assertTrue 判断给定的布尔值是否为 true
assertFalse 判断给定的布尔值是否为 false
assertNull 判断给定的对象引用是否为 null
assertNotNull 判断给定的对象引用是否不为 null

3.1.1 assertEquals() & assertNotEquals()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码@SpringBootTest
@DisplayName("测试断言机制")
public class SpringBoot_Assert {

int sum(int i, int j) {
return i + j;
}

@Test
@DisplayName("测试简单断言1:assertEquals")
void testSimpleAssertions1() {
int cal = sum(1, 5);
// 报错
Assertions.assertEquals(5, cal);
// Nothing happen
Assertions.assertNotEquals(6, cal, "sum()不符合预期、");
}

}

3.1.2 assertSame() & assertNotSame()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码@SpringBootTest
@DisplayName("测试断言机制")
public class SpringBoot_Assert {
@Test
@DisplayName("测试简单断言2:assertSame")
void testSimpleAssertions2() {
Object obj1 = new Object();
Object obj2 = new Object();
Assertions.assertNotSame(obj1, obj2);

Assertions.assertSame(obj1, obj2, "obj1!=obj2");

}
}

3.1.3 assertTrue/False() & assertNull/NotNull()

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
java复制代码@SpringBootTest
@DisplayName("测试断言机制")
public class SpringBoot_Assert {

boolean returnFlag() {
return false;
}

@Test
@DisplayName("测试简单断言3:assertTrue、assertNull")
void testSimpleAssertions() {
Object obj = null;

/**
* org.opentest4j.AssertionFailedError: 不为True ==>
* 期望:true
* 实际 :false
*/
Assertions.assertTrue(returnFlag(), "不为True");

/**
* org.opentest4j.AssertionFailedError: 着实为Null ==> expected: not <null>
*/
Assertions.assertNotNull(obj, "着实为Null");
}
}

3.2 数组断言

通过 assertArrayEquals 方法来判断两个对象或原始类型的数组是否相等。

1
2
3
4
5
java复制代码@Test
@DisplayName("测试数组数组断言:assertArrayEquals")
void testAssertArrayEquals() {
Assertions.assertArrayEquals(new int[]{1, 2, 3}, new int[]{3, 1, 2});
}

3.3 组合断言

assertAll 方法接受多个 org.junit.jupiter.api.Executable 函数式接口的实例作为要验证的断言,可以通过 lambda 表达式很容易的提供这些断言。

3.4 异常断言

在 JUnit4 时期,想要测试方法的异常情况时,需要用 @Rule 注解的 ExpectedException 变量还是比较麻烦的。而 JUnit5 提供了一种新的断言方式 Assertions.assertThrows(),配合函数式编程就可以进行使用。

1
2
3
4
5
java复制代码@Test
@DisplayName("异常断言:assertThrows")
void testAssertThrows() {
Assertions.assertThrows(ArithmeticException.class, () -> System.out.println(1 / 0));
}

3.5 超时断言

Junit5 还提供了 Assertions.assertTimeout() 为测试方法设置了超时时间。

1
2
3
4
5
java复制代码@Test
@DisplayName("超时异常:assertTimeout")
void testAssertTimeout() {
Assertions.assertTimeout(Duration.ofMillis(500), () -> Thread.sleep(1000));
}

3.6 快速失败

通过 fail 方法直接使得测试失败。

1
2
3
4
5
6
7
java复制代码@Test
@DisplayName("快速失败:fail")
void testFail() {
if (1 == 2) {
Assertions.fail("This should fail...");
}
}

  1. 前置条件

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

1
2
3
4
5
6
7
8
9
10
java复制代码@SpringBootTest
public class SpringBoot_Sumptions {
@Test
void testSumptions() {
Assumptions.assumeTrue(false, "false..");

System.out.println("后续..");
}

}

并没有打印 “后续..” 语句且无报错:

  1. 嵌套测试

JUnit 5 可以通过 Java 中的内部类和 @Nested 注解实现嵌套测试,从而可以更好地将相关的测试方法组织在一起。在内部类中可以使用 @BeforeEach@AfterEach 注解,而且嵌套的层次没有限制。

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
java复制代码@SpringBootTest
@DisplayName("嵌套测试")
public class SpringBoot_Nested {

Stack<Object> stack;

@Test
void testNested1() {
System.out.println("Do nothing...");

// org.opentest4j.AssertionFailedError: stack is null.. ==> expected: not <null>
// Assertions.assertNotNull(stack, "stack is null..");
}

@Nested // @Nested不适用于方法
@DisplayName("When new")
class whenNew {
@BeforeEach
void initialize() {
stack = new Stack<>();
}

@Test
void testNested2() {
Assertions.assertNotNull(stack, "stack is null..");
}

@Test
@DisplayName("throws EmptyStackException when popped")
void throwsExceptionWhenPopped() {
Assertions.assertThrows(EmptyStackException.class, stack::pop);
}

@Nested
@DisplayName("after pushing an element")
class afterPushing {
String anElement = "an element";

@BeforeEach
void pushAnElement() {
stack.push(anElement);
}

@Test
@DisplayName("returns the element when popped and then stack is empty")
void returnElementWhenPopped() {
Assertions.assertEquals(anElement, stack.pop());
Assertions.assertTrue(stack.isEmpty());
}
}

}

}
  1. 参数化测试

参数化测试是 JUnit5 很重要的一个新特性,它使得用不同的参数多次运行测试成为了可能,也为我们的单元测试带来许多便利。

利用 @ValueSource 等注解指定入参,我们将可以使用不同的参数进行多次单元测试,而不需要每新增一个参数就新增一个单元测试,省去了很多冗余代码。

  • @ValueSource:为参数化测试指定入参来源,支持八大基础类以及 String 类型,Class 类型
  • @NullSource:表示为参数化测试提供一个 null 的入参
  • @EnumSource:表示为参数化测试提供一个枚举入参
  • @CsvFileSource:表示读取指定 CSV 文件内容作为参数化测试入参
  • @MethodSource:表示读取指定方法的返回值作为参数化测试入参(注意方法返回需要是一个流)

当然如果参数化测试仅仅只能做到指定普通的入参还达不到令人惊艳的地步。

⭐它真正的强大之处的地方在于可以支持外部的各类入参。如:CSV,YML,JSON 文件甚至方法的返回值也可以作为入参。只需要去实现 ArgumentsProvider 接口,任何外部文件都可以作为它的入参。

6.1 @ValueSource & @ParameterizedTest

1
2
3
4
5
6
7
8
9
10
11
java复制代码@SpringBootTest
public class SpringBoot_ParameterTest {

@ParameterizedTest
@DisplayName("参数化测试1: @ValueSource")
@ValueSource(ints = {1, 2, 3, 4, 5})
void testParameters1(int i) {
System.out.println(i);
}

}

6.2 @MethodSource & @ParameterizeTest

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码@SpringBootTest
public class SpringBoot_ParameterTest {

// 必须是static静态方法~
String[] parameterProvider() {
return new String[]{"hello", "world", "!"};
}

@ParameterizedTest
@DisplayName("参数化测试2: @MethodSource")
@MethodSource("parameterProvider") // 指定方法名
void testParameters2(String arr) {
System.out.println(arr);
}

}

  1. JUnit 迁移指南

在进行迁移的时候需要注意如下的变化:

  • 注解在 org.junit.jupiter.api 包中,断言在 org.junit.jupiter.api.Assertions 类中,前置条件在 org.junit.jupiter.api.Assumptions 类中
  • @Before@After 替换成 @BeforeEach@AfterEach
  • @BeforeClass@AfterClass 替换成 @BeforeAll@AfterAll
  • @Ignore 替换成 @Disabled
  • @Category 替换成 @Tag
  • @RunWith@Rule@ClassRule 替换成 @ExtendWith

希望本文对你有所帮助🧠

欢迎在评论区留下你的看法🌊,我们一起讨论与分享🔥

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

0%