JUnit5學習之一:基本操作

歡迎訪問我的GitHub

https://github.com/zq2599/blog_demos

內容:所有原創文章分類彙總及配套源碼,涉及Java、Docker、Kubernetes、DevOPS等;

關於《JUnit5學習》系列

《JUnit5學習》系列旨在通過實戰提升SpringBoot環境下的單元測試技能,一共八篇文章,鏈接如下:

  1. 基本操作
  2. Assumptions類
  3. Assertions類
  4. 按條件執行
  5. 標籤(Tag)和自定義註解
  6. 參數化測試(Parameterized Tests)基礎
  7. 參數化測試(Parameterized Tests)進階
  8. 綜合進階(終篇)

本篇概覽

本文是《JUnit5學習》系列的第一篇,通過實戰學習在SpringBoot框架下JUnit5的基本功能,全篇章節如下:

  1. JUnit5簡介
  2. SpringBoot對JUnit5的依賴
  3. 常用註解簡介
  4. 5版本已廢棄的註解介紹
  5. 進入實戰環節,先介紹版本和環境信息
  6. 創建《JUnit5學習》系列源碼的父工程
  7. 創建子工程,編碼體驗常用註解

關於JUnit5

  1. JUnit是常用的java單元測試框架,<font color="blue">5</font>是當前最新版本,其整體架構如下(圖片來自網絡):

在這裏插入圖片描述 2. 從上圖可見,整個JUnit5可以劃分成三層:頂層框架(Framework)、中間的引擎(Engine),底層的平臺(Platform); 3. 官方定義JUnit5由三部分組成:Platform、Jupiter、Vintage,功能如下; 4. Platform:位於架構的最底層,是JVM上執行單元測試的基礎平臺,還對接了各種IDE(例如IDEA、eclipse),並且還與引擎層對接,定義了引擎層對接的API; 5. Jupiter:位於引擎層,支持5版本的編程模型、擴展模型; 6. Vintage:位於引擎層,用於執行低版本的測試用例;

  • 可見整個Junit Platform是開放的,通過引擎API各種測試框架都可以接入;

SpringBoot對JUnit5的依賴

  1. 這裏使用SpringBoot版本爲<font color="blue">2.3.4.RELEASE</font>,在項目的pom.xml中依賴JUnit5的方法如下:
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>
  1. 如下圖紅框,可見JUnit5的jar都被<font color="blue">spring-boot-starter-test</font>間接依賴進來了:

在這裏插入圖片描述

曾經的RunWith註解

  1. 在使用JUnit4的時候,咱們經常這麼寫單元測試類:
@RunWith(SpringRunner.class)
@SpringBootTest
public class XXXTest {
  1. 對於上面的RunWith註解,JUnit5官方文檔的說法如下圖紅框所示,已經被<font color="blue">ExtendWith</font>取代:

在這裏插入圖片描述 3. 咱們再來看看SpringBootTest註解,如下圖,可見已經包含了<font color="blue">ExtendWith</font>:

在這裏插入圖片描述 4. 綜上所述,SpringBoot+JUnit5時,RunWith註解已經不需要了,正常情況下僅SpringBootTest註解即可,如果對擴展性有更多需求,可以添加ExtendWith註解,如下圖:

在這裏插入圖片描述

常用的JUnit5註解(SpringBoot環境)

注意,接下來提到的<font color="red">測試方法</font>,是指當前class中所有被@Test、@RepeatedTest、@ParameterizedTest、@TestFactory修飾的方法;

  1. ExtendWith:這是用來取代舊版本中的RunWith註解,不過在SpringBoot環境如果沒有特別要求無需額外配置,因爲SpringBootTest中已經有了;
  2. Test:被該註解修飾的就是測試方法;
  3. BeforeAll:被該註解修飾的必須是靜態方法,會在所有測試方法之前執行,會被子類繼承,取代低版本的BeforeClass;
  4. AfterAll:被該註解修飾的必須是靜態方法,會在所有測試方法執行之後才被執行,會被子類繼承,取代低版本的AfterClass;
  5. BeforeEach:被該註解修飾的方法會在每個測試方法執行前被執行一次,會被子類繼承,取代低版本的Before;
  6. AfterEach:被該註解修飾的方法會在每個測試方法執行後被執行一次,會被子類繼承,取代低版本的Before;
  7. DisplayName:測試方法的展現名稱,在測試框架中展示,支持emoji;
  8. Timeout:超時時長,被修飾的方法如果超時則會導致測試不通過;
  9. Disabled:不執行的測試方法;

5版本已廢棄的註解

以下的註解都是在5之前的版本使用的,現在已經被廢棄:

被廢棄的註解 新的繼任者
Before BeforeEach
After AfterEach
BeforeClass BeforeAll
AfterClass AfterAll
Category Tag
RunWith ExtendWith
Rule ExtendWith
ClassRule RegisterExtension

版本和環境信息

整個系列的編碼和執行在以下環境進行,供您參考:

  1. 硬件配置:處理器i5-8400,內存32G,硬盤128G SSD + 500G HDD
  2. 操作系統:Windows10家庭中文版
  3. IDEA:2020.2.2 (Ultimate Edition)
  4. JDK:1.8.0_181
  5. SpringBoot:2.3.4.RELEASE
  6. JUnit Jupiter:5.6.2 接下來開始實戰,咱們先建好SpringBoot項目;

關於lombok

爲了簡化代碼,項目中使用了lombok,請您在IDEA中安裝lombok插件;

源碼下載

  1. 如果您不想編碼,可以在GitHub下載所有源碼,地址和鏈接信息如下表所示(https://github.com/zq2599/blog_demos):
名稱 鏈接 備註
項目主頁 https://github.com/zq2599/blog_demos 該項目在GitHub上的主頁
git倉庫地址(https) https://github.com/zq2599/blog_demos.git 該項目源碼的倉庫地址,https協議
git倉庫地址(ssh) [email protected]:zq2599/blog_demos.git 該項目源碼的倉庫地址,ssh協議
  1. 這個git項目中有多個文件夾,本章的應用在<font color="blue">junitpractice</font>文件夾下,如下圖紅框所示:

在這裏插入圖片描述

  1. <font color="blue">junitpractice</font>是父子結構的工程,本篇的代碼在<font color="red">junit5experience</font>子工程中,如下圖:

在這裏插入圖片描述

創建Maven父工程

  1. 爲了便於管理整個系列的源碼,在此建立名爲<font color="blue">junitpractice</font>的maven工程,後續所有實戰的源碼都作爲junitpractice的子工程;
  2. junitpractice的pom.xml如下,可見是以SpringBoot的<font color="blue">2.3.4.RELEASE</font>版本作爲其父工程:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <modules>
        <module>simplebean</module>
        <!--
        <module>testenvironment</module>
        -->
    </modules>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.4.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.bolingcavalry</groupId>
    <artifactId>junitpractice</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>pom</packaging>
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
                <version>1.16.16</version>
            </dependency>
        </dependencies>
    </dependencyManagement>
</project>

本篇的源碼工程

接下來咱們準備一個簡單的SpringBoot工程用於做單元測試,該工程有service和controller層,包含一些簡單的接口和類;

  1. 創建名爲<font color="blue">junit5experience</font>的子工程,pom.xml如下,注意單元測試要依賴<font color="blue">spring-boot-starter-test</font>:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>com.bolingcavalry</groupId>
        <artifactId>junitpractice</artifactId>
        <version>1.0-SNAPSHOT</version>
        <relativePath>../pom.xml</relativePath>
    </parent>
    <groupId>com.bolingcavalry</groupId>
    <artifactId>junit5experience</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>junit5experience</name>
    <description>Demo project for simplebean in Spring Boot junit5</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

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

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webflux</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>
  1. 寫一些最簡單的業務代碼,首先是service層的接口HelloService.java:
package com.bolingcavalry.junit5experience.service;

public interface HelloService {
    String hello(String name);
    int increase(int value);
    /**
     * 該方法會等待1秒後返回true,這是在模擬一個耗時的遠程調用
     * @return
     */
    boolean remoteRequest();
}
  1. 上述接口對應的實現類如下,hello和increase方法分別返回String型和int型,remoteRequest故意sleep了1秒鐘,用來測試Timeout註解的效果:
package com.bolingcavalry.junit5experience.service.impl;

import com.bolingcavalry.junit5experience.service.HelloService;
import org.springframework.stereotype.Service;

@Service()
public class HelloServiceImpl implements HelloService {
    @Override
    public String hello(String name) {
        return "Hello " + name;
    }

    @Override
    public int increase(int value) {
        return value + 1;
    }

    @Override
    public boolean remoteRequest() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException interruptedException) {
            interruptedException.printStackTrace();
        }

        return true;
    }
}
  1. 添加一個簡單的controller:
package com.bolingcavalry.junit5experience.controller;

import com.bolingcavalry.junit5experience.service.HelloService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {

    @Autowired
    private HelloService helloService;

    @RequestMapping(value = "/{name}", method = RequestMethod.GET)
    public String hello(@PathVariable String name){
        return helloService.hello(name);
    }
}
  1. 啓動類:
package com.bolingcavalry.junit5experience;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Junit5ExperienceApplication {

    public static void main(String[] args) {
        SpringApplication.run(Junit5ExperienceApplication.class, args);
    }
}
  • 以上就是一個典型的web工程,接下來一起爲該工程編寫單元測試用例;

編寫測試代碼

  1. 在下圖紅框位置新增單元測試類:

在這裏插入圖片描述 2. 測試類的內容如下,涵蓋了剛纔提到的常用註解,請注意每個方法的註釋說明:

package com.bolingcavalry.junit5experience.service.impl;

import com.bolingcavalry.junit5experience.service.HelloService;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.concurrent.TimeUnit;
import static org.assertj.core.api.Assertions.assertThat;

@SpringBootTest
@Slf4j
class HelloServiceImplTest {

    private static final String NAME = "Tom";

    @Autowired
    HelloService helloService;

    /**
     * 在所有測試方法執行前被執行
     */
    @BeforeAll
    static void beforeAll() {
        log.info("execute beforeAll");
    }

    /**
     * 在所有測試方法執行後被執行
     */
    @AfterAll
    static void afterAll() {
        log.info("execute afterAll");
    }

    /**
     * 每個測試方法執行前都會執行一次
     */
    @BeforeEach
    void beforeEach() {
        log.info("execute beforeEach");
    }

    /**
     * 每個測試方法執行後都會執行一次
     */
    @AfterEach
    void afterEach() {
        log.info("execute afterEach");
    }

    @Test
    @DisplayName("測試service層的hello方法")
    void hello() {
        log.info("execute hello");
        assertThat(helloService.hello(NAME)).isEqualTo("Hello " + NAME);
    }

    /**
     * DisplayName中帶有emoji,在測試框架中能夠展示
     */
    @Test
    @DisplayName("測試service層的increase方法\uD83D\uDE31")
    void increase() {
        log.info("execute increase");
        assertThat(helloService.increase(1)).isEqualByComparingTo(2);
    }

    /**
     * 不會被執行的測試方法
     */
    @Test
    @Disabled
    void neverExecute() {
        log.info("execute neverExecute");
    }

    /**
     * 調用一個耗時1秒的方法,用Timeout設置超時時間是500毫秒,
     * 因此該用例會測試失敗
     */
    @Test
    @Timeout(unit = TimeUnit.MILLISECONDS, value = 500)
    @Disabled
    void remoteRequest() {
        assertThat(helloService.remoteRequest()).isEqualTo(true);
    }
}
  1. 接下來執行測試用例試試,點擊下圖紅框中的按鈕:

在這裏插入圖片描述 4. 如下圖,在彈出的菜單中,點擊紅框位置:

在這裏插入圖片描述

  1. 執行結果如下,可見Displayname註解的值作爲測試結果的方法名展示,超時的方法會被判定爲測試不通過,Disable註解修飾的方法則被標記爲跳過不執行:

在這裏插入圖片描述

  1. 在父工程junitpractice的pom.xml文件所在目錄,執行<font color="blue">mvn test</font>命令,可以看到maven執行單元測試的效果:

在這裏插入圖片描述

  • 至此,咱們對SpringBoot環境下的JUnit5有了最基本的瞭解,接下來的章節會展開更多知識點和細節,對單元測試做更深入的學習。

你不孤單,欣宸原創一路相伴

  1. Java系列
  2. Spring系列
  3. Docker系列
  4. kubernetes系列
  5. 數據庫+中間件系列
  6. DevOps系列

歡迎關注公衆號:程序員欣宸

微信搜索「程序員欣宸」,我是欣宸,期待與您一同暢遊Java世界... https://github.com/zq2599/blog_demos

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章