6

JUnit5学习之一:基本操作

 3 years ago
source link: https://segmentfault.com/a/1190000039227508
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

欢迎访问我的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>是当前最新版本,其整体架构如下(图片来自网络):

iANFvaB.png!mobile

  1. 从上图可见,整个JUnit5可以划分成三层:顶层框架(Framework)、中间的引擎(Engine),底层的平台(Platform);
  2. 官方定义JUnit5由三部分组成:Platform、Jupiter、Vintage,功能如下;
  3. Platform:位于架构的最底层,是JVM上执行单元测试的基础平台,还对接了各种IDE(例如IDEA、eclipse),并且还与引擎层对接,定义了引擎层对接的API;
  4. Jupiter:位于引擎层,支持5版本的编程模型、扩展模型;
  5. 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>间接依赖进来了:

zmYZnmY.jpg!mobile

曾经的RunWith注解

  1. 在使用JUnit4的时候,咱们经常这么写单元测试类:
@RunWith(SpringRunner.class)
@SpringBootTest
public class XXXTest {
  1. 对于上面的RunWith注解,JUnit5官方文档的说法如下图红框所示,已经被<font color="blue">ExtendWith</font>取代:

FR7biau.jpg!mobile

  1. 咱们再来看看SpringBootTest注解,如下图,可见已经包含了<font color="blue">ExtendWith</font>:

BVJvQzZ.jpg!mobile

  1. 综上所述,SpringBoot+JUnit5时,RunWith注解已经不需要了,正常情况下仅SpringBootTest注解即可,如果对扩展性有更多需求,可以添加ExtendWith注解,如下图:

ZF3A3aY.jpg!mobile

常用的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/blo...
名称 链接 备注 项目主页 https://github.com/zq2599/blo... 该项目在GitHub上的主页 git仓库地址(https) https://github.com/zq2599/blo... 该项目源码的仓库地址,https协议 git仓库地址(ssh) [email protected]:zq2599/blog_demos.git 该项目源码的仓库地址,ssh协议
  1. 这个git项目中有多个文件夹,本章的应用在<font color="blue">junitpractice</font>文件夹下,如下图红框所示:

qMnyamy.jpg!mobile

  1. <font color="blue">junitpractice</font>是父子结构的工程,本篇的代码在<font color="red">junit5experience</font>子工程中,如下图:

3AjUJfv.jpg!mobile

创建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. 在下图红框位置新增单元测试类:

aEvYFbA.jpg!mobile

  1. 测试类的内容如下,涵盖了刚才提到的常用注解,请注意每个方法的注释说明:
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. 接下来执行测试用例试试,点击下图红框中的按钮:

FN3eAvj.jpg!mobile

  1. 如下图,在弹出的菜单中,点击红框位置:

mM3y2iJ.jpg!mobile

  1. 执行结果如下,可见Displayname注解的值作为测试结果的方法名展示,超时的方法会被判定为测试不通过,Disable注解修饰的方法则被标记为跳过不执行:

mEVzEr6.jpg!mobile

  1. 在父工程junitpractice的pom.xml文件所在目录,执行<font color="blue">mvn test</font>命令,可以看到maven执行单元测试的效果:

6F7nAvi.jpg!mobile

  • 至此,咱们对SpringBoot环境下的JUnit5有了最基本的了解,接下来的章节会展开更多知识点和细节,对单元测试做更深入的学习。

你不孤单,欣宸原创一路相伴


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK