1、JUnit5 的变化

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提供了JUnit5的新的编程模型,是JUnit5新特性的核心。内部 包含了一个测试引擎,用于在Junit Platform上运行。 JUnit Vintage: 由于JUint已经发展多年,为了照顾老的项目,JUnit Vintage提供了兼容JUnit4.x,Junit3.x的测试引擎。 07、单元测试 - 图1 注意: SpringBoot 2.4 以上版本移除了默认对 Vintage 的依赖。如果需要兼容junit4需要自行引入(不能使用junit4的功能 @Test 该注解在Junit4和5中都有 JUnit 5’s Vintage Engine Removed from **spring-boot-starter-test,如果需要继续兼容junit4需要自行引入vintage**

想兼容,需引入

  1. <dependency>
  2. <groupId>org.junit.vintage</groupId>
  3. <artifactId>junit-vintage-engine</artifactId>
  4. <scope>test</scope>
  5. <exclusions>
  6. <exclusion>
  7. <groupId>org.hamcrest</groupId>
  8. <artifactId>hamcrest-core</artifactId>
  9. </exclusion>
  10. </exclusions>
  11. </dependency>

**

image.png

现在版本:
引入

  1. <dependency>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-starter-test</artifactId>
  4. <scope>test</scope>
  5. </dependency>
  1. @SpringBootTest
  2. class Boot05WebAdminApplicationTests {
  3. @Test
  4. void contextLoads() {
  5. }
  6. }

SpringBoot整合Junit以后。

  • 编写测试方法:@Test标注(注意需要使用junit5版本的注解)
  • Junit类(@SpringBootTest标注的类)具有Spring的功能,@Autowired、比如 @Transactional 标注测试方法,测试完成后自动回滚

以前:
@SpringBootTest + @RunWith(SpringTest.class)

2、JUnit5常用注解

JUnit5的注解与JUnit4的注解有所变化
JUnit5官方文档

  • @Test : 表示方法是测试方法。但是与JUnit4的@Test不同,他的职责非常单一不能声明任何属性,拓展的测试将会由Jupiter提供额外测试
  • @ParameterizedTest : 表示方法是参数化测试,下方会有详细介绍
  • @RepeatedTest : 表示方法可重复执行,也就是指定测试次数,会重复指定次数,下方会有详细介绍
  • @DisplayName : 为测试类或者测试方法设置展示名称
  • @BeforeEach : 表示在每个单元测试之前执行,这两个会在每一个单元测试前后都执行一次
  • @AfterEach : 表示在每个单元测试之后执行

image.png

  • @BeforeAll : 表示在所有单元测试之前执行, 这两个修饰方法必须是静态方法,并且只会在所有方法执行前执行一次,下面一个类似
  • @AfterAll : 表示在所有单元测试之后执行

image.png

  • @Tag : 表示单元测试类别,类似于JUnit4中的@Categories
  • @Disabled : 表示测试类或测试方法不执行,类似于JUnit4中的@Ignore ,测试报告上展示的是skip
  • @Timeout : 表示测试方法运行如果超过了指定时间将会返回错误,用于验证是否可以在指定时间内运行
  • @ExtendWith : 为测试类或测试方法提供扩展类引用
    • JUnit5不具有Spring Boot的自动注入功能,也就是说不能从容器中自动获取组件对象,想要用Spring boot的容器功能,需要在类上加@SpringBootTest注解

image.png

  • SpringBootTest注解是一个复合注解,里面是用ExtendWith注解进行扩展的SpringBoot

image.png

  1. import org.junit.jupiter.api.Test; //注意这里使用的是jupiter的Test注解!!
  2. public class TestDemo {
  3. @Test
  4. @DisplayName("第一次测试")
  5. public void firstTest() {
  6. System.out.println("hello world");
  7. }

3、断言(assertions)

断言(assertions)是测试方法中的核心部分,用来对测试需要满足的条件进行验证这些断言方法都是 org.junit.jupiter.api.Assertions 的静态方法。JUnit 5 内置的断言可以分成如下几个类别:
检查业务逻辑返回的数据是否合理。
所有的测试运行结束以后,会有一个详细的测试报告;

1、简单断言

如果前面的断言失败了,后面的代码不会执行
用来对单个值进行简单的验证。如:

方法 说明
assertEquals 判断两个对象或两个原始类型是否相等
assertNotEquals 判断两个对象或两个原始类型是否不相等
assertSame 判断两个对象引用是否指向同一个对象
assertNotSame 判断两个对象引用是否指向不同的对象
assertTrue 判断给定的布尔值是否为 true
assertFalse 判断给定的布尔值是否为 false
assertNull 判断给定的对象引用是否为 null
assertNotNull 判断给定的对象引用是否不为 null
  1. @Test
  2. @DisplayName("simple assertion")
  3. public void simple() {
  4. assertEquals(3, 1 + 2, "simple math");// 第三个参数是断言错误时自定义提示内容
  5. assertNotEquals(3, 1 + 1);
  6. assertNotSame(new Object(), new Object());
  7. Object obj = new Object();
  8. assertSame(obj, obj);
  9. assertFalse(1 > 2);
  10. assertTrue(1 < 2);
  11. assertNull(null);
  12. assertNotNull(new Object());
  13. }

2、数组断言

通过 assertArrayEquals 方法来判断两个对象或原始类型的数组是否相等
是逻辑相等,也就是equals,不是地址相等,即==对于数组就是数组元素一致

  1. @Test
  2. @DisplayName("array assertion")
  3. public void array() {
  4. assertArrayEquals(new int[]{1, 2}, new int[] {1, 2});
  5. }

3、组合断言

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

  1. @Test
  2. @DisplayName("assert all")
  3. public void all() {
  4. assertAll("Math", // 第一个参数是名字,可以不写。
  5. () -> assertEquals(2, 1 + 1),
  6. () -> assertTrue(1 > 0)
  7. );
  8. }

4、异常断言

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

  1. @Test
  2. @DisplayName("异常测试")
  3. public void exceptionTest() {
  4. // 第一个参数是预期出现的异常类型,第二个参数是需要判断的业务逻辑,第三个参数是不符合第一个异常时抛出的msg
  5. ArithmeticException exception = Assertions.assertThrows(
  6. //扔出断言异常
  7. ArithmeticException.class, () -> System.out.println(1 % 0));
  8. }

5、超时断言

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

  1. @Test
  2. @DisplayName("超时测试")
  3. public void timeoutTest() {
  4. //如果测试方法时间超过1s将会异常
  5. Assertions.assertTimeout(Duration.ofMillis(1000), () -> Thread.sleep(500));
  6. }

6、快速失败

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

  1. @Test
  2. @DisplayName("fail")
  3. public void shouldFail() {
  4. fail("This should fail");
  5. }

跑单元测试,生成测试报告:
image.png

4、前置条件(assumptions)

JUnit 5 中的前置条件(assumptions【假设】)类似于断言,不同之处在于不满足的断言会使得测试方法失败(测试报告上展示的是Errors/Failure,和@Disabled效果一样),而不满足的前置条件只会使得测试方法的执行终止(测试报告上展示的是skip)。前置条件可以看成是测试方法执行的前提,当该前提不满足时,就没有继续执行的必要。

  1. @DisplayName("前置条件")
  2. public class AssumptionsTest {
  3. private final String environment = "DEV";
  4. @Test
  5. @DisplayName("simple")
  6. public void simpleAssume() {
  7. assumeTrue(Objects.equals(this.environment, "DEV"));
  8. assumeFalse(() -> Objects.equals(this.environment, "PROD"));
  9. }
  10. @Test
  11. @DisplayName("assume then do")
  12. public void assumeThenDo() {
  13. assumingThat(
  14. Objects.equals(this.environment, "DEV"),
  15. () -> System.out.println("In DEV")
  16. );
  17. }
  18. }

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

5、嵌套测试

JUnit 5 可以通过 Java 中的内部类和@Nested 注解实现嵌套测试,从而可以更好的把相关的测试方法组织在一起。在内部类中可以使用@BeforeEach @AfterEach 注解,而且嵌套的层次没有限制。
加在里层的测试类上面,表示嵌套测试。对于before/after只对于同层或者更里层的测试有效,对于外层不生效

  1. @DisplayName("A stack")
  2. class TestingAStackDemo {
  3. Stack<Object> ;
  4. @Test //第一层test
  5. @DisplayName("is instantiated with new Stack()")
  6. void isInstantiatedWithNew() {
  7. new Stack<>();
  8. // 嵌套测试的情况下,外层的Test不能驱动内层的@Before(After)Each/ALL之类的方法提前/之后运行
  9. // 所以运行到这儿 stack 为NULL
  10. }
  11. @Nested // 指定下面是嵌套测试 第二层Test
  12. @DisplayName("when new")
  13. class WhenNew {
  14. @BeforeEach
  15. void createNewStack() {
  16. stack = new Stack<>();
  17. }
  18. @Test
  19. @DisplayName("is empty")
  20. void isEmpty() {
  21. assertTrue(stack.isEmpty());// 运行成功,因为 @BeforeEach createNewStack() 已经提前运行并生成一个空栈
  22. }
  23. @Test
  24. @DisplayName("throws EmptyStackException when popped")
  25. void throwsExceptionWhenPopped() {
  26. assertThrows(EmptyStackException.class, stack::pop); // 运行成功, 空栈不能pop
  27. }
  28. @Test
  29. @DisplayName("throws EmptyStackException when peeked")
  30. void throwsExceptionWhenPeeked() {
  31. assertThrows(EmptyStackException.class, stack::peek); // 运行成功, 空栈不能peek
  32. }
  33. @Nested // 嵌套测试 第三层Test
  34. @DisplayName("after pushing an element")
  35. class AfterPushing {
  36. String anElement = "an element";
  37. @BeforeEach
  38. void pushAnElement() {
  39. stack.push(anElement); // stack push一个元素 ,内层的Test可以驱动外层的@Before(After)Each/ALL之类的方法提前/之后运行
  40. // 所以stack在上一层已经创建好了
  41. }
  42. @Test
  43. @DisplayName("it is no longer empty")
  44. void isNotEmpty() {
  45. assertFalse(stack.isEmpty()); // 运行成功,因为 @BeforeEach pushAnElement() 已经提前Push了一个元素
  46. }
  47. @Test
  48. @DisplayName("returns the element when popped and is empty")
  49. void returnElementWhenPopped() {
  50. assertEquals(anElement, stack.pop()); // 运行成功
  51. assertTrue(stack.isEmpty()); // 运行成功
  52. }
  53. @Test
  54. @DisplayName("returns the element when peeked but remains not empty")
  55. void returnElementWhenPeeked() {
  56. assertEquals(anElement, stack.peek()); // 运行成功
  57. assertFalse(stack.isEmpty()); // 运行成功 peek没有弹出,不为空
  58. }
  59. }
  60. }
  61. }

6、参数化测试

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

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

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

当然如果参数化测试仅仅只能做到指定普通的入参还达不到让我觉得惊艳的地步。让我真正感到他的强大之处的地方在于他可以支持外部的各类入参。如:CSV,YML,JSON 文件甚至方法的返回值也可以作为入参。只需要去实现ArgumentsProvider接口,任何外部文件都可以作为它的入参。

  1. @ParameterizedTest // 参数化测试
  2. @ValueSource(strings = {"one", "two", "three"}) // 遍历测试参数
  3. @DisplayName("参数化测试1")
  4. public void parameterizedTest1(String string) {
  5. System.out.println(string);
  6. Assertions.assertTrue(StringUtils.isNotBlank(string));
  7. }
  8. @ParameterizedTest
  9. @MethodSource("stringProvider") //指定方法名
  10. @DisplayName("方法来源参数")
  11. public void testWithExplicitLocalMethodSource(String name) { // 遍历测试方法返回的结果
  12. System.out.println(name);
  13. Assertions.assertNotNull(name);
  14. }
  15. static Stream<String> stringProvider() { //
  16. return Stream.of("apple", "banana");
  17. }

7、JUnit4版本迁移到JUnit5指南(看官网文档)

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