SpringBoot 从入门到光头 第七章 单元测试


SpringBoot 从入门到光头 —— 第七章 单元测试


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 提供了 JUnit5 的新的编程模型,是 JUnit5 新特性的核心。内部 包含了一个 测试引擎 ,用于在 Junit Platform 上运行。

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

Junit5

注意⚠️:SpringBoot 2.4 以上版本移除了默认对 Vintage 的依赖,如果需要兼容 JUnit 4 需要自行引入(不能使用 Junit 4 的功能 @Test

JUnit 5’s Vintage Engine Removed from spring-boot-starter-test,如果需要继续兼容 Junit 4 需要自行引入 Vintage

pom.xml

<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>

JUnit 测试环境引入

pom.xml

<dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
</dependency>

JunitDepedency

现在的版本:

com.yourname.adminserver.test.AdminServerApplicationTest

@SpringBootTest
class AdminServerApplicationTests &#123;
    @Test
    void test1() &#123;

    &#125;
&#125;

以前的版本:@SpringBootTest + @RunWith(SpringRunner.class)

SpringBoot 整合 JUnit 以后:

  • 编写测试方法:@Test 标注(注意需要使用 JUnit 5 版本的注解)
  • JUnit 类具有 Spring 的功能,例如可以使用 @Autowired 自动装配、@Transactional 标注测试方法,测试完成后自动回滚

2. JUnit 5 常用注解

JUnit 5 的注解与JUnit 4 的注解相比有所变化:

https://junit.org/junit5/docs/current/user-guide/#writing-tests-annotations

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

示例代码:

com.yourname.adminserver.test.Junit5Test

/**
 * &#123;@code @BootstrapWith(SpringBootTestContextBootstrapper.class)&#125;<br/>
 * &#123;@code @ExtendWith(SpringExtension.class)&#125;
 *
 * @author gregPerlinLi
 * @since 2021-11-21
 */
@SpringBootTest
@DisplayName("Junit 5 function test class")
public class Junit5Test &#123;
    @Autowired
    JdbcTemplate jdbcTemplate;
    @Test
    @DisplayName(value = "Test @DisplayName annotation")
    void testDisplayName() &#123;
        System.out.println(1);
        System.out.println(jdbcTemplate);
    &#125;
    @Test
    @Disabled
    @DisplayName(value = "Another test")
    void test2() &#123;
        System.out.println(2);
    &#125;
    @Test
    @RepeatedTest(5)
    void test3() &#123;
        System.out.println(5);
    &#125;
    @BeforeEach
    void testBeforeEach() &#123;
        System.out.println("The test is beginning...");
    &#125;
    @AfterEach
    void testAfterEach() &#123;
        System.out.println("The test is ended...");
    &#125;
    @BeforeAll
    static void testBeforeAll() &#123;
        System.out.println("All test will begin...");
    &#125;
    @AfterAll
    static void testAfterAll() &#123;
        System.out.println("All test is completed...");
    &#125;
    /**
     * Specify the timeout time of the method, and throw an exception when the time is exceeded
     *
     * @throws InterruptedException InterruptedException
     */
    @Test
    @Timeout(value = 500, unit = TimeUnit.MILLISECONDS)
    void testTimeOut() throws InterruptedException &#123;
        Thread.sleep(600);
    &#125;
&#125;

3. 断言(Assertions

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

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

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

3.1. 简单断言

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

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

示例代码:

com.yourname.adminserver.test.Junit5Test

/**
 * @author gregPerlinLi
 * @since 2021-11-21
 */
@SpringBootTest
@DisplayName("Junit 5 function test class")
public class Junit5Test &#123;
    /**
     * Assertions: Previous assertions fail, and subsequent code are not executed
     */
    @Test
    @DisplayName(value = "Test simple assertions")
    void testSimpleAssertions() &#123;
        int cal = cal(2, 3);
        assertEquals(5, cal, "Business logic calculation failed");
        Object obj1 = new Object();
        Object obj2 = new Object();
        assertSame(obj1, obj2, "The two objects are different");
    &#125;
&#125;

3.2. 数组断言

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

示例代码:

com.yourname.adminserver.test.Junit5Test

/**
 * @author gregPerlinLi
 * @since 2021-11-21
 */
@SpringBootTest
@DisplayName("Junit 5 function test class")
public class Junit5Test &#123;
    @Test
    @DisplayName(value = "Test array assertions")
    void testArrayAssertions() &#123;
        assertArrayEquals(new int[]&#123;1, 2&#125;, new int[]&#123;1, 2&#125;, "The two array contents are different");
    &#125;
&#125;

3.3. 组合断言

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

示例代码:

com.yourname.adminserver.test.Junit5Test

/**
 * @author gregPerlinLi
 * @since 2021-11-21
 */
@SpringBootTest
@DisplayName("Junit 5 function test class")
public class Junit5Test &#123;
    @Test
    @DisplayName(value = "Test combination assertions")
    void testCombinationAssertions() &#123;
        assertAll("test",
                () -> assertTrue(true && true, "The result is not true"),
                () -> assertEquals(1, 1, "The result is not 1"));
        System.out.println("OK...");
    &#125;
&#125;

3.4. 异常断言

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

示例代码:

com.yourname.adminserver.test.Junit5Test

/**
 * @author gregPerlinLi
 * @since 2021-11-21
 */
@SpringBootTest
@DisplayName("Junit 5 function test class")
public class Junit5Test &#123;
    @Test
    @DisplayName(value = "Test excepted assertions")
    void testExceptedAssertions() &#123;
        // Conclude that the business logic must throw exception
        assertThrows(ArithmeticException.class,
                () -> &#123;int i = 10 / 0;&#125;,
                "Normal operation of business logic!");
    &#125;
&#125;

3.5. 超时断言

Junit5 提供了Assertions.assertTimeout() 为测试方法设置了超时时间,如果超时则断定失败

示例代码:

com.yourname.adminserver.test.Junit5Test

/**
 * @author gregPerlinLi
 * @since 2021-11-21
 */
@SpringBootTest
@DisplayName("Junit 5 function test class")
public class Junit5Test &#123;
    @Test
    @DisplayName(value = "Test overtime assertions")
    public void testTimeoutAssertions() &#123;
        // If the test method takes more than 1s, it will throw exception
        Assertions.assertTimeout(Duration.ofMillis(1000),
                () -> Thread.sleep(500),
                "Overtime!");
    &#125;
&#125;

3.6. 快速失败

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

示例代码:

com.yourname.adminserver.test.Junit5Test

/**
 * @author gregPerlinLi
 * @since 2021-11-21
 */
@SpringBootTest
@DisplayName("Junit 5 function test class")
public class Junit5Test &#123;
    @Test
    @DisplayName(value = "Test error")
    public void testError() &#123;
        if ( 0 == 0) &#123;
            fail("Test failed");
        &#125;
    &#125;
&#125;

4. 前置条件(Assumptions

JUnit 5 中的前置条件(Assumptions【假设】)类似于断言,不同之处在于 不满足的断言会使得测试方法失败 ,而不满足的 前置条件只会使得测试方法的执行终止 。前置条件可以看成是测试方法执行的前提,当该前提不满足时,就没有继续执行的必要。

示例代码:

com.yourname.adminserver.test.Junit5Test

/**
 * @author gregPerlinLi
 * @since 2021-11-21
 */
@DisplayName("Test assumptions")
public class testAssumptions &#123;
        private final String environment = "DEV";
        
        @Test
        @DisplayName("simple")
        public void simpleAssume() &#123;
           assumeTrue(Objects.equals(this.environment, "DEV"));
           assumeFalse(() -> Objects.equals(this.environment, "PROD"));
        &#125;
        @Test
        @DisplayName("assume then do")
        public void assumeThenDo() &#123;
           assumingThat(
              Objects.equals(this.environment, "DEV"),
              () -> System.out.println("In DEV")
           );
        &#125;
&#125;

5. 嵌套测试

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

示例代码:

com.yourname.adminserver.test.StackDemonTest

/**
 * @author gregPerlinLi
 * @since 2021-11-21
 */
@DisplayName("Nested test")
public class StackDemoTest &#123;
        Stack<Object> stack;

    @Test
    @DisplayName(value = "is instantiated with new Stack()")
    void isInstantiatedWithNew() &#123;
        new Stack<>();
        // Under the nested test, the test of the outer layer cannot start the methods such as Before(After)Each/All of the inner layer to run in before/after
        // assertNotNull(stack);
    &#125;
    @Nested
    @DisplayName(value = "when new")
    class WhenNew &#123;
        @BeforeEach
        void createNewStack() &#123;
            stack = new Stack<>();
        &#125;
        @Test
        @DisplayName(value = "is empty")
        void isEmpty() &#123;
            assertTrue(stack.isEmpty());
        &#125;
        @Test
        @DisplayName(value = "throws EmptyStackException when popped")
        void throwsExceptionWhenPopped() &#123;
            assertThrows(EmptyStackException.class, stack::pop);
        &#125;
        @Test
        @DisplayName(value = "throws EmptyStackException when peeked")
        void throwsExceptionWhenPeeked() &#123;
            assertThrows(EmptyStackException.class, stack::peek);
        &#125;
        @Nested
        @DisplayName(value = "after pushing an element")
        class AfterPushing &#123;
            String anElement = "an element";

            @BeforeEach
            void pushAnElement() &#123;
                stack.push(anElement);
            &#125;
            /**
             * The inner test can start the methods such as Before(After)Each/All of the outer layer to run in before/after
             */
            @Test
            @DisplayName(value = "it is no longer empty")
            void isNotEmpty() &#123;
                assertFalse(stack.isEmpty());
            &#125;
            @Test
            @DisplayName(value = "returns the element when popped and is empty")
            void returnElementWhenPopped() &#123;
                assertEquals(anElement, stack.pop());
                assertTrue(stack.isEmpty());
            &#125;
            @Test
            @DisplayName(value = "returns the element when peeked but remains not empty")
            void returnElementWhenPeeked() &#123;
                assertEquals(anElement, stack.peek());
                assertFalse(stack.isEmpty());
            &#125;
        &#125;
    &#125;
&#125;

6. 参数化测试

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

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

@ValueSource为参数化测试指定入参来源,支持八大基础类以及 String 类型,Class类型

@NullSource表示为参数化测试提供一个 null 的入参

@EnumSource表示为参数化测试提供一个枚举入参

@CsvFileSource表示读取指定 CSV 文件内容作为参数化测试入参

@MethodSource表示读取指定方法的返回值作为参数化测试入参(注意方法返回需要是一个流)

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

示例代码:

com.yourname.adminserver.test.ParameterTest

/**
 * @author gregPerlinLi
 * @since 2021-11-21
 */
public class ParameterTest &#123;
    @ParameterizedTest
    @DisplayName(value = "Parameter test")
    @ValueSource(ints = &#123;1, 2, 3, 4, 5&#125;)
    void testParameterizedTest(int i) &#123;
        System.out.println(i);
    &#125;
    @ParameterizedTest
    @DisplayName(value = "Parameter test")
    @MethodSource(value = "stringProvider")
    void testParameterizedTest2(String i) &#123;
        System.out.println(i);
    &#125;
    static Stream<String> stringProvider() &#123;
        return Stream.of("apple", "orange", "banana");
    &#125;
&#125;

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


文章作者: gregPerlinLi
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 gregPerlinLi !
  目录