1. JUnit5 简介
SpringBoot 2.2.0 版本开始引入 JUnit5 作为单元测试默认库
作为新版本的 JUnit 框架,JUnit5 与之前的版本的 JUnit 框架有很大的不同。由三个不同子项目的几个不同模块组成。
JUnit5 = JUnit Platform + JUnit Jupiter + JUnit Vintage
- JUnit Platform :JUnit Platform 是在 JVM 上启动测试框架的的基础,不仅支持 JUnit 自制的测试引擎,其他测试引擎也可以接入
- JUnit Jupiter :JUnit Jupiter 提供了 JUnit5 的新的编程模型,是 JUnit5 新特性的核心。内部包含了一个测试引擎,用于在 JUnit5 Platform 上运行。
- JUnit Vintage :由于 JUnit 已经发展多年,为了照顾老项目,JUnit Vintage 提供了兼容 JUnit4.x,JUnit3.x 的测试引擎。
注意:SpringBoot 2.4 以上版本默认移除了 JUnit Vintage 的依赖。如果需要兼容 JUnit4 需要自行引入。如果要继续兼容 JUnit4 需要自行引入 JUnit Vintage。
<dependency>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-core</artifactId>
</exclusion>
</exclusions>
</dependency>
单元测试的依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
现在版本使用方式:
@SpringBootTest
class Boot05Web01ApplicationTests {
@Test
void contextLoads() {
}
}
之前版本使用方式:
@SpringBootTest
@RunWith(SpringTest.class)
class XxxTests {
}
SpringBoot 整合 Junit 后
- 编写测试方法:@Test 标注(注意使用 Junit5 版本的注解)
- Junit 类具有 Spring 的功能, @Autowired、@Transaction 等 标注的测试方法
2.Junit5 常用注解
- @Test :表示方法是测试方法。但是与 JUnit4 不同,他的职责非常单一不能声明任何属性,拓展的测试将会有 Jupiter 提供额外测试。
- @ParameterizedTest:表示方法是参数化测试,下发有详细介绍。
- @RepeatedTest:表示方法可重复执行,下方会有详细介绍。
- @DisplayName:为测试类或者测试方法设置展示名称
- @BeforeEach:表示在每个单元测试之前执行。
- @AfterEach:表示在每个单元测试之后执行。
- @BeforeAll:表示在所有单元测试之前执行。
- @AfterAll:表示在所有单元测试之后执行。
- @Tag:表示单元测试类别,类似于 Junit4 中的 @Categories
- @Disabled:表示测试或测试方法不执行,雷速与 Junit4 中的 @Ignore
- @Timeout:表示测试方法运行如果超过了指定时间将会返回错误
- @ExtendWith:为测试类或测试方法提供扩展类引用
@SpringBootTest //@SpringBootTest 就是一个复合注解,它内部包含 @ExtendWith({SpringExtension.class})。加上此注解表示支持 SpringBoot 功能
@DisplayName("测试 Junit5")
public class Junit5Test {
@DisplayName("测试 testDisplayName")
@Test
void testDisplayName() {
System.out.println(1);
}
@Test
void test2() {
System.out.println(2);
}
@RepeatedTest(value = 4)
@Test
void testRepeatedTest(){
System.out.println(3);
}
@Disabled
@Test
void testDisabled() {
System.out.println(5);
}
@Timeout(1) //执行时间超过1秒异常 java.util.concurrent.TimeoutException
@Test
void testTimeout() throws InterruptedException {
Thread.sleep(1100);
}
/**
* @ValueSource 注解为每次执行传入的参数
* 本次输出的结果:
* 每次开始前执行
* 11
* 每次结束执行
*
* 每次开始前执行
* 22
* 每次结束执行
*
* 每次开始前执行
* 33
* 每次结束执行
*/
@ParameterizedTest
@ValueSource(strings={"11","22","33","44","55"})
void testParameterizedTest(String param) {
System.out.println(param);
}
@BeforeEach
void testBeforeEach() {
System.out.println("每次开始前执行");
}
@AfterEach
void testAfterEach() {
System.out.println("每次结束执行");
}
//@BeforeAll 标注的方法必须是 static
@BeforeAll
static void testBeforeAll(){
System.out.println("全部开始前执行");
}
//@AfterAll 标注的方法必须是 static
@AfterAll
static void testAfterAll() {
System.out.println("全部结束后执行");
}
}
3.断言
断言 Assertion 是测试方法中的核心部分,用来对测试需要满足的条件进行验证。这些断言的方法都是 org.junit.jupiter.api.Assertions 的静态方法。检查业务逻辑返回的数据是否合理。所有的测试运行结束以后,会有一个详细的测试报告。所有推荐使用断言机制。
3.1 简单断言
用来验证对单个值进行简单的验证。如:
方法 | 说明 |
assertEquals | 判断两个对象或两个原始类型是否相等 |
assertNotEquals | 判断两个对象或两个原始类型是否不相等 |
assertSame | 判断两个对象引用是否指向同一个对象 |
assertNotSame | 判断两个对象引用是否指向不同对象 |
assertTrue | 判断给定的布尔值是否为 true |
assertFalse | 判断给定的布尔值是否为 false |
assertNull | 判断给定的对象引用是否为 null |
assertNotNull | 判断给定的对象引用是否不为 null |
/**
* 断言:前面断言失败,后面的代码不会继续执行
*/
@Test
@DisplayName("测试简单断言")
void testSimpleAssertions() {
int v = add(2,3);
//判定相等
assertEquals(5,v);
// assertEquals(6,v,"不相等");
assertNotEquals(6,v);
Object obj1 = new Object();
Object obj2 = new Object();
// assertSame(obj1,obj2,"两个对象不一样");
assertNotSame(obj1,obj2);
assertTrue(true);
assertFalse(false);
assertNull(null);
assertNotNull(obj1);
}
int add(int i, int j){
return i+j;
}
3.2 数组断言
@Test
@DisplayName("测试数组断言")
void testArray() {
assertArrayEquals(new int[]{1,2}, new int[]{1,2});
}
3.3 组合断言
/**
* 只有所有断言成功,才能往下走
*/
@Test
@DisplayName("测试组合断言")
void testAll() {
assertAll(
()->assertTrue(true),
()->assertEquals(1,2)
);
}
3.4 异常断言
@Test
@DisplayName("测试异常断言")
void testException() {
//断定业务逻辑一定出现异常
assertThrows(ArithmeticException.class, ()->{
int i=10/0;
},"业务逻辑居然正常");
}
3.5 超时断言
@Timeout(1) //执行时间超过1秒异常 java.util.concurrent.TimeoutException
@Test
void testTimeout() throws InterruptedException {
Thread.sleep(1100);
}
3.6 快速失败
@Test
@DisplayName("测试快速失败")
void testFail() {
fail("测试失败");
}
4.前置条件
Junit5 中的前置条件(assumptions 【假设】)类似于断言,不同支出在于不满足的断言 assertions 会使得测试方法失败,而不满足前置条件只会使得测试方法执行终止。
@DisplayName("测试前置条件")
@Test
void testAssumptions() {
Assumptions.assumeTrue(false,"结果不是 true");
}
它和 @Disable 的效果类似,如下图。
5.嵌套测试
Junit5 可以通过 Java 中的内部类和 @Nested 注解实现嵌套测试,从而可以更好地把相关的测试方法组织在一起。在内部类中可以使用 @BeforeEach 和 @AfterEach 注解,而且嵌套层次没有限制。
嵌套测试情况下:
- 外侧的Test不能驱动内层类的Before(After)Each/All 之类的方法。
- 内层的Test可以驱动外侧的 Before(After)Each/All 之类的方法
@DisplayName("A stack")
class TestingAStackDemo {
Stack<Object> stack;
@Test
@DisplayName("is instantiated with new Stack()")
void isInstantiatedWithNew() {
new Stack<>();
//嵌套测试情况下,外侧的Test不能驱动内层类的Before(After)Each/All 之类的方法。
assertNotNull(stack);
}
@Nested
@DisplayName("when new")
class WhenNew {
@BeforeEach
void createNewStack() {
stack = new Stack<>();
}
@Test
@DisplayName("is empty")
void isEmpty() {
assertTrue(stack.isEmpty());
}
@Test
@DisplayName("throws EmptyStackException when popped")
void throwsExceptionWhenPopped() {
assertThrows(EmptyStackException.class, stack::pop);
}
@Test
@DisplayName("throws EmptyStackException when peeked")
void throwsExceptionWhenPeeked() {
assertThrows(EmptyStackException.class, stack::peek);
}
@Nested
@DisplayName("after pushing an element")
class AfterPushing {
String anElement = "an element";
@BeforeEach
void pushAnElement() {
stack.push(anElement);
}
//内层的Test可以驱动外侧的 Before(After)Each/All 之类的方法
@Test
@DisplayName("it is no longer empty")
void isNotEmpty() {
assertFalse(stack.isEmpty());
}
@Test
@DisplayName("returns the element when popped and is empty")
void returnElementWhenPopped() {
assertEquals(anElement, stack.pop());
assertTrue(stack.isEmpty());
}
@Test
@DisplayName("returns the element when peeked but remains not empty")
void returnElementWhenPeeked() {
assertEquals(anElement, stack.peek());
assertFalse(stack.isEmpty());
}
}
}
}
6.参数化测试
参数化测试是 Junit5 很重要的一个特性,它使得不同的参数多次运行测试成为了可能,也为我们的单元测试带来许多便利。
利用 @ValueSource 等注解,指定入参,我们将可以使用不同的参数进行多次单元测试,而不需要每新增一个参数就新增一个单元测试,省去了很多冗余代码。
- @ValueSource :为参数化测试指定入参来源,支持八大基础类型以及 String、Class 类型。
- @NullSource:表示为参数化测试提供一个 null 的入参。
- @EnumSource:表示为参数化测试提供一个枚举入参。
- @CsvFileSource:表示读取指定 CSV 文件内容作为参数化测试入参。
- @MethodSource:表示读取指定方法的返回值作为参数化测试入参(注意方法返回需要是一个流)。
当然如果参数化测试仅仅只能做到指定普通的入参还达不到让我们觉得经验的地步。它真正强大之处在于它可以支持外部的各类入参。如:CSV、YML、JSON 文件 甚至方法的返回值也可以作为入参。只需要去实现【ArgumentsProvider】接口,任何外部文件都可以作为它的入参。
@DisplayName("参数化测试")
public class TestParamTest {
@DisplayName("测试 ValueSource")
@ParameterizedTest
@ValueSource(ints = {1,2,3,4,5})
void testParameterized(int i) {
System.out.println(i);
}
@DisplayName("测试 ValueSource")
@ParameterizedTest
@MethodSource("method") //指定方法名称
void testParameterized2(String str) {
System.out.println(str);
}
static Stream<String> method() {
return Stream.of("apple", "banana");
}
}
7.迁移指南
在迁移的时候注意如下:
- 注解在 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。