14.1 自动化测试概述

14.1.1 编写测试代码的重要性

在日常的开发过程中,为了保证代码质量,软件工程师应该对自己编写的代码进行充分的测试,这种测试不仅仅是体现在对正常功能的简单接口调用,而是要根据代码中的各种逻辑分支,进行尽可能多的覆盖性单元测试以及主要逻辑的集成测试。 在做这些测试的时候绝不应该仅仅依赖于 Postman 这样的工具,而要以编写独立的单元/集成测试代码的方式来实现。

编写测试代码对提高工作效率、软件质量及自身编程水平来说都是一种非常有用的手段。这个过程有助于提前发现软件Bug、重新审视所写代码并进行优化。

尽管编写测试代码作为开发方法的一部分有如此的收益,但在我们日常的工作中,仍然存在不少项目它们的测试代码是不完整要么是缺失的。常见的理由往往是代码逻辑过于复杂;写单元测试时耗费的时间较长;任务重、工期紧,或者干脆就不写了。

14.1.2 单元测试与集成测试

1. 单元测试

单元测试是一种孤立地测试尽可能小的代码片段的测试。那么,什么是一个单元?你源代码的中一个单元就是逻辑上与其余代码分离的最小代码片段。它是一个完整的且逻辑上不同的代码片段,而且是最小的部分。
在大多数编程语言中,你的单元会是一个函数或方法调用。

单元测试的好处是编写测试就相当容易。这种易编写性意味着你可以在开发功能时完成单元测试。

与其它形式的测试相比,单元测试的执行时间相当短。这意味着你可以频繁运行单元测试。随着软件的成熟,一套单元测试是防止回归和降低维护成本的有力工具。单元测试不能验证应用程序代码是否正确地与外部依赖项一起工作。

就典型的Spring Boot CRUD 应用来说,可以编写单元测试来分别测试 REST控制器,DAO层等。甚至不需要嵌入式服务器。我们之前编写的直接测试 MyBatis 映射方法的那些测试代码就属于针对 DAO 层的单元测试。

2. 集成测试

单元测试的一个关键假设是,被测试的软件很容易分成不同的单元。在没有考虑单元测试编写的软件中,这个假设很少成立。向现有软件添加单元测试通常是一种非常好的方法,来稳定软件并防止将来回归,但是重构代码来支持简单的单元测试可能需要大量工作,甚至会引入新的缺陷。在考虑将单元测试添加到现有软件时,需要考虑成本和收益。如果你的代码正在工作,如果代码很少需要修改,如果代码不容易进行单元测试,那么加入单元测试的好处可能无法保证成本。在这些情况下,可以依靠集成测试来防止该领域的缺陷。

如果单元测试的哲学是基于这样一种认识,即测试小的独立代码片段是防止回归的一种好方法,那么集成测试是基于这样一种理解,即事情通常在边缘状态出错。外部世界是一个混乱的地方,它与你代码交互的地方通常是意外发生的地方。

所以一旦开发并集成了不同的模块,便需要执行集成测试。集成测试聚焦于整个软件栈,其主要目的是发现不同模块相互交互以端对端处理用户请求时的问题。

一个好的集成测试策略应该关注较少数量的高影响测试。这些测试应该跨越单元测试无法跨越的所有界限,写入文件系统,接触外部资源,等等。

在集成测试中,将测试从控制器到持久层的完整请求处理。应用程序嵌入式服务器中运行,以创建应用程序上下文和所有bean。这些bean中的某些可能会被覆盖以模拟某些行为。

集成测试既可以根据测试内容将整个应用程序做为测试对象,也可以仅将某些组件进行集成测试。他们可能需要为数据库实例和分配资源。尽管也可以模拟这些交互以提高测试性能。集成测试的场景可以如此地分散,以至于有人会把对组件或者某个具体 REST 的集成测试理解为单元测试。

3. 比较之后的小结

将测试划分为单元和集成两个明确的类别有点太简单了,如果我们只关注定义,我们就会忽略目标,即正确的工作软件。

一些非常有想法的开发者认为单元测试可以并且应该读写数据库。其它人则认为单元测试是一种浪费,大粒度的集成测试提供的价值最大

那究竟应该首选哪种类型的测试呢?答案是单靠两者中的任一个都是不够的。这两者都是综合测试计划的一部分。

每种情况都是独特的,基于在其它情况下有效的建议不应盲目遵循。需要牢记的一个问题是,这个测试要捕获什么类型的缺陷。如果每个测试都是经过深思熟虑编写来提升软件可靠性的,如果测试在不再有价值时被删除,那么随着时间的推移,将发现为特定项目提供最大价值的特定测试方法。

你可以通过单元测试实现 100%代码覆盖率,但仍然发现你的软件失败。你可能试图从错误的位置读取文件,或者你的软件可能从一个调用的服务得到预期之外的输出,或者它可能以一种无效的方式调用数据库。

在 Spring Boot Reference Documentation 的 7.8.2 Testing Spring Applications 中有这样一段话

Often, you need to move beyond unit testing and start integration testing (with a Spring ApplicationContext). It is useful to be able to perform integration testing without requiring deployment of your application or needing to connect to other infrastructure.

翻译成近似的中文就是:通常,你需要超越单元测试并开始集成测试 (使用SpringApplicationContext)。能够执行集成测试而无需部署应用程序或需要连接到其他基础设施是很有用的。

所以目前建议放弃不必要的单元测试,拥抱集成测试(功能测试)和切片测试(本章未涉及)。

总之,集成测试越接近真实世界的交互,就越有可能发现问题并提供真正的价值。

14.2 Spring Boot 的测试能力

14.2.1 关于依赖库

Spring Boot 提供了许多实用工具注解以帮助测试应用程序,而且在我们初始化项目的时候,已经通过spring-boot-starter-test 依赖自动导入了。

  1. <dependency>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-starter-test</artifactId>
  4. <scope>test</scope>
  5. </dependency>

这个 starter 会自动导入下面的内容

依赖库 作用简介
Spring Boot Test Spring Boot心 测试的核心项目
Spring Boot Test Autoconfigure 支持测试的自动配置
JUnit 5 用于单元测试Java应用程序的事实标准
Mockito Java 模拟(mock)框架
AssertJ 断言库,提供了流式的断言方式
Hamcrest 匹配对象库(也称为约束或谓词)
JSONassert JSON的断言库
JsonPath JSON的XPath

JUnit 5 的 vintage(复古)引擎可用于运行 JUnit 4 的测试代码,但这仅仅是为了兼容以往的代码。鉴于所有新开发的代码都不应该使用 JUnit 4,因此你可以声明除去它。

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

14.2.2 不同类型的注解

为了避免繁琐难懂的 XML 配置,Spring Test 引入了大量的注解进行系统配置,从而大大减轻了配置工作量。理解这些注解非常重要。毫不夸张的说,使用 Spring Boot Test 就是使用相关的注解。

Spring Boot Test 中的注解主要分如下几类

类型 注解 说明
配置类型 @TestConfiguration 等 提供一些测试相关的配置入口
mock 类型 @MockBean 等 提供 mock 支持
启动测试 @SpringBootTest 、 @*Test 以 Test 结尾的注解,具有加载 applicationContext 的能力
自动配置 @AutoConfigure* 以 AutoConfigure 开头,具有加载测试支持功能的能力。

14.2.3 Web测试环境类型

Spring Boot 提供了一个 @SpringBootTest 注解在 Spirng Boot 应用程序中标记测试用类,它自动侦测并加载 @SpringBootApplication@SpringBootConfiguration 中的配置。使用 @SpringBootTest 后,Spring 将加载所有被管理的 bean基本等同于启动了整个服务

@SpringBootTest 有如下的属性

配置名称 说明
value 指定配置属性
properties 指定配置属性,和 value 意义相同
classes 指定配置类,等同于 @ContextConfiguration 中的 class,若没有显示指定,将查找嵌套的 @Configuration 类,然后返回到 SpringBootConfiguration 搜索配置
webEnvironment 指定 web 环境,可选值有:MOCK、RANDOM_PORT、DEFINED_PORT、NONE

下面是 webEnvironment 可选项的详细说明:

可选值 说明
MOCK 此为默认值。该类型提供一个 mock 环境,内嵌的servlet 容器(Tomcat 应用服务器)并没有真正启动,也不会监听 web 端口。
RANDOM_PORT 用内嵌的servlet 容器(Tomcat 应用服务器)启动一个真实的 web 服务,监听一个随机端口。
DEFINED_PORT 用内嵌的servlet 容器(Tomcat 应用服务器)启动一个真实的 web 服务,监听定义好的端口(从配置中读取)。
NONE 启动一个非 web 的 ApplicationContext,既不提供 mock 环境,也不提供真是的 web 服务。

不管你如何设置,如果当前服务的 classpath 中没有包含 web 相关的依赖,@SpringBootTest 将启动一个非 web 的 ApplicationContext,此时指定 webEnvironment 将失去作用

14.3 Spring Boot 常用测试方法

13.3.1 准备一个待测试的 API
下面是准备测试的 API 的代码

  1. package com.longser.union.cloud.controller;
  2. import com.longser.restful.annotation.IgnoreRestful;
  3. import org.springframework.web.bind.annotation.GetMapping;
  4. import org.springframework.web.bind.annotation.RestController;
  5. @RestController
  6. public class HelloWorld {
  7. @IgnoreRestful
  8. @GetMapping("/test/hello-spring")
  9. public String helloWorld() {
  10. return "Hello Spring Boot.";
  11. }
  12. }

14.3.1 用 Mock(模仿)环境测试

这是 @SpringBootTest 的默认模式。它加载 Web ApplicationContext 并提供 Mock(模拟) Web 环境。此时内嵌的 Servlet 容器(Tomcat 应用服务器)没有真正启动,也不会监听 Web 服务端口。如果你的类路径上没有可用的 Web 环境,则此模式透明地退回到创建常规的非 Web ApplicationContext。

使用 @AutoConfigureMockMvc 注解可以指定 Web应用程序测试的具体手段为 MockMvc,并且提供可以自动注入的 MockMvc Bean。下面是使用 MockMvc 方法执行测试的范例

  1. package com.longser.union.cloud.tutorials;
  2. import org.junit.jupiter.api.Test;
  3. import org.springframework.beans.factory.annotation.Autowired;
  4. import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
  5. import org.springframework.boot.test.context.SpringBootTest;
  6. import org.springframework.test.web.servlet.MockMvc;
  7. import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
  8. import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
  9. import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
  10. @SpringBootTest
  11. @AutoConfigureMockMvc
  12. class MyMockMvcTests {
  13. private @Autowired MockMvc mvc;
  14. @Test
  15. void testWithMockMvc() throws Exception {
  16. mvc.perform(get("/test/hello-spring"))
  17. .andExpect(status().isOk())
  18. .andExpect(content().string("Hello Spring Boot."));
  19. }
  20. }

在模拟环境中进行测试通常比使用完整的 servlet 容器运行要快。 但是,由于模拟发生在 Spring MVC 层,因此无法直接使用 MockMvc 测试依赖于较低级别 servlet 容器行为的代码。例如,Spring Boot 的错误处理是基于 servlet 容器提供的“错误页面”支持。 这意味着,虽然可以按预期测试你的 MVC 层抛出和处理异常,但无法直接测试是否呈现了特定的自定义错误页面。 如果需要测试这些较低级别的问题,可以启动一个完全运行的服务器。

尽管在 Mock 环境中使用@AutoConfigureWebTestClient 可以自动注入 WebTestClient Bean,但它需要很多额外的依赖,比如运行下面的测试代码

  1. package com.longser.union.cloud.tutorials;
  2. import org.junit.jupiter.api.Test;
  3. import org.springframework.beans.factory.annotation.Autowired;
  4. import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient;
  5. import org.springframework.boot.test.context.SpringBootTest;
  6. import org.springframework.test.web.reactive.server.WebTestClient;
  7. @SpringBootTest()
  8. @AutoConfigureWebTestClient
  9. class MyMockWebTestClientTests {
  10. @Autowired
  11. WebTestClient webClient;
  12. @Test
  13. void testWithWebTestClient() {
  14. webClient
  15. .get()
  16. .uri("/test/hello-spring")
  17. .exchange()
  18. .expectStatus().isOk()
  19. .expectBody(String.class)
  20. .isEqualTo("Hello Spring Boot.");
  21. }
  22. }

你会得到内容很多的关于 Negative matches 的列表,里面都是当前项目中没有依赖的内容。要解决这个问题需要为此增加很多额外的依赖。因此,再 Mock 环境中,还是使用 MockMvc 吧。

下面是与 Mock 相关的常见注解

注解 作用
@MockBean 用于 mock 指定的 class 或被注解的属性
@MockBeans 使 @MockBean 支持在同一类型或属性上多次出现
@SpyBean 用于 spy 指定的 class 或被注解的属性
@SpyBeans 使 @SpyBeans 支持在同一类型或属性上多次出现

@MockBean 和 @SpyBean 这两个注解,在 mockito 框架中本来已经存在,Spring Boot Test 又定义一份重复的注解,目的在于使 MockBean 和 SpyBean 被 ApplicationContext 管理,从而方便使用。

这两个注解功能基本相同,都能模拟方法的各种行为。他们的不同之处在于 MockBean 是全新的对象,跟正式对象没有关系;而 SpyBean 与正式对象紧密联系,可以模拟正式对象的部分方法,没有被模拟的方法仍然可以运行正式代码。

注意:如果你测试的方法使用了 @Transactional 事务注解,在默认的 Mock 环境中它会在每个测试方法结束时回滚事务(即不需要 @Rollback 注解)。

14.3.2 用嵌入的务器进行测试

设置 webEnvironment 参数为 RANDOM_PORT 可以启动嵌入的Servlet 容器(Tomcat 应用服务器),并在一个随机的端口上监听。 它加载 WebServerApplicationContext 并提供真正的Web环境。

在控制台窗口可以看到 Tomcat 会在一个随机的端口启动。

  1. Tomcat started on port(s): 58914 (http) with context path ''

你可以从日志输出或从 WebServerApplicationContext 通过其 WebServer 访问服务器正在运行的端口。 获得它并确保它已初始化的最佳方法是添加类型为 ApplicationListener 的 @Bean 并在发布时将容器从事件中拉出。更简单方便的的方式是通过使用 @LocalServerPort 注解直接将实际端口注入到变量中:

  1. package com.longser.union.cloud.tutorials;
  2. import org.junit.jupiter.api.Test;
  3. import org.springframework.boot.test.context.SpringBootTest;
  4. import org.springframework.boot.web.server.LocalServerPort;
  5. import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.*;
  6. @SpringBootTest(webEnvironment = RANDOM_PORT)
  7. class MyLocalPortTests {
  8. @LocalServerPort
  9. int localServerPort;
  10. @Test
  11. void testLocalServerPort() {
  12. System.out.println("The local server port is " + localServerPort);
  13. }
  14. }

@LocalServerPort 是 @Value(“${local.server.port}”) 的元注解。 不要尝试在常规应用程序中注入端口。 正如我们刚刚看到的,该值仅在容器初始化后才设置。 与测试相反,应用程序代码回调会提前处理(在值实际可用之前)。

和 Mock 环境不同,当 webEnvironmentRANDOM_PORT 或 DEFINED_PORT 时, 再我们的项目中可以使用 @AutoConfigureWebTestClient 注解完成自动配置并获得 WebTestClient Bean,因此下面的代码可以成功运行:

  1. package com.longser.union.cloud.tutorials;
  2. import org.junit.jupiter.api.Test;
  3. import org.springframework.beans.factory.annotation.Autowired;
  4. import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient;
  5. import org.springframework.boot.test.context.SpringBootTest;
  6. import org.springframework.test.web.reactive.server.WebTestClient;
  7. import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.*;
  8. @SpringBootTest(webEnvironment = RANDOM_PORT)
  9. @AutoConfigureWebTestClient()
  10. class MyWebTestClientTests {
  11. @Autowired
  12. WebTestClient webClient;
  13. @Test
  14. void testWithWebTestClient() {
  15. webClient
  16. .get()
  17. .uri("/test/hello-spring")
  18. .exchange()
  19. .expectStatus().isOk()
  20. .expectBody(String.class)
  21. .isEqualTo("Hello Spring Boot.");
  22. }
  23. }

注意:由于 RANDOM_PORT 或 DEFINED_PORT 模式隐式地提供了一个真正的 servlet 环境,HTTP客户端和服务器在单独的线程和事务中运行,在服务器上启动的任何事务都不会回滚。所以需要显式样的标记 @Rollback 注解配合 @Transactional 实现测试的后的自动回滚。

14.3.3 与Spring Security的集成

Spring Security 支持以特定用户身份运行测试。 例如,下面代码段中的测试将使用具有 ADMIN 角色的经过身份验证的用户运行。

  1. import org.junit.jupiter.api.Test;
  2. import org.springframework.beans.factory.annotation.Autowired;
  3. import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
  4. import org.springframework.security.test.context.support.WithMockUser;
  5. import org.springframework.test.web.servlet.MockMvc;
  6. import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
  7. @WebMvcTest(UserController.class)
  8. class MySecurityTests {
  9. @Autowired
  10. private MockMvc mvc;
  11. @Test
  12. @WithMockUser(roles = "ADMIN")
  13. void requestProtectedUrlWithUser() throws Exception {
  14. this.mvc.perform(get("/"));
  15. }
  16. }

除了 @WithMockUser 注解外,Spring Security 还提供了 @WithAnonymousUser、@WithUserDetails、@WithSecurityContext 等注解用于测试。

14.4 应用 JUnit 5 执行测试

我们在讨论 MyBatis 的时候已经使用 JUnit 5 编写了2个单元测试程序。

如果你使用的是JUnit 5,则无需添加等效的 @ExtendWith(SpringExtension.class) 因为其他测试用的注解已经用它标注过(尽管 JUnit 5的 vintage(复古)引擎可用于运行 JUnit 4 的测试代码,但这仅仅是为了兼容以往的代码。所有新开发的代码都不应该使用 JUnit 4)。

14.4.1 JUnit 5 概述

Junit5 由 JUnit PlatformJUnit JupiterJUnit Vintage 这3部分构成:

  • JUnit Platform
    这是 JUnit 提供的平台功能模块,通过它,其它的测试引擎都可以接入 JUnit 实现接口和执行。其主要作用是在 JVM 上启动测试框架。它定义了一个抽象的 TestEngine API 来定义运行在平台上的测试框架。其他的自动化测试引擎或开发人员⾃⼰定制的引擎都可以接入 Junit 实现对接和执行。同时还支持通过命令行、Gradle 和 Maven 来运行平台(这对于我们做自动化测试至关重要)
  • JUnit JUpiter
    这是JUnit 5的核心,是一个基于JUnit Platform的引擎实现,它包含许多丰富的新特性来使得自动化测试更加方便和强大。可以看作是承载 JUnit4 原有功能的演进,包含了 JUnit 5 最新的编程模型和扩展机制。很多丰富的新特性使 JUnit ⾃动化测试更加方便、功能更加丰富和强大,也是测试需要重点学习的地方。JUpiter 本身也是⼀一个基于 JUnit Platform 的引擎实现,对 JUnit 5 而言,JUnit JUpiter API 只是另一个 AP。
  • JUnit Vintage
    这个模块是兼容 JUnit 3、JUnit 4 版本的测试引擎,使得旧版本的自动化测试也可以在 JUnit5 下正常运行。JUnit 发展了10数年,JUnit 3 和 JUnit 4 都积累了大量的⽤用户,作为新一代框 架,这个模块是对 JUnit 3,JUnit 4 版本兼容的测试引擎,使旧版本 JUnit 的⾃动化测试脚本也可以顺畅运行在 JUnit 5 下,它也可以看作是基于 JUnit Platform 实现的引擎范例。

JUnit 5 有如下的特点

  • JUnit5中支持lambda表达式,语法简单且代码不冗余。
  • JUnit5易扩展,包容性强,可以接入其他的测试引擎。
  • 功能更强大提供了新的断言机制、参数化测试、重复性测试等新功能。

JUnit5的 新特性有:嵌套单元测试Lambda支持参数化测试重复测试动态测试

下面是 JUnit 5 与 JUnit 4 中的注解比较

Junit5 Junit4 说明
@Test @Test 被注解的方法是一个测试方法。与 JUnit 4 相同。
@BeforeAll @BeforeClass 被注解的(静态)方法将在当前类中的所有 @Test 方法前执行一次。
@BeforeEach @Before 被注解的方法将在当前类中的每个 @Test 方法前执行。
@AfterEach @After 被注解的方法将在当前类中的每个 @Test 方法后执行。
@AfterAll @AfterClass 被注解的(静态)方法将在当前类中的所有 @Test 方法后执行一次。
@Disabled @Ignore 被注解的方法不会执行(将被跳过),但会报告为已执行。

JUnit 5 对 Java 运行环境的最低要求是 Java 8。另外,JUnit 5不仅仅用于单元测试。

14.4.2 常用注解说明

下面是 Junit 5 的常用注解

注解 说明
@Test 表明一个测试方法
@DisplayName(“name”) 测试类或方法的显示名称
@BeforeEach 表明在单个测试方法运行之前执行的方法
@AfterEach 表明在单个测试方法运行之后执行的方法
@BeforeAll 表明在所有测试方法运行之前执行的方法(只执行一次)
@AfterAll 表明在所有测试方法运行之后执行的方法(只执行一次)
@Disabled 禁用测试类或方法
@Tag 为测试类或方法添加标签
@RepeatedTest(n) 额外重复执行,即执行n次
@Nested 嵌套测试
@ParameterizedTest 参数化测试
@ValueSource(ints = {1, 2, 3}) 参数化测试提供数据

14.4.3 常用注解实操演示

1. 执行公共方法

下面是 @Test、@BeforeEach、@AfterEach、@BeforeAll、@AfterAll 关系的示例

  1. package com.longser.union.cloud.tutorials;
  2. import org.junit.jupiter.api.*;
  3. public class TestJunit5demo {
  4. @BeforeAll
  5. static void beforeAl1() {
  6. System.out.println("我是@BeforeAl1,测试类执行前要先执行我");
  7. }
  8. @BeforeEach
  9. void beforeEachTest() {
  10. System.out.println("我是@BeforeEach,执行用例前先执行我");
  11. }
  12. @Test
  13. void test1() {
  14. System.out.println("JUnit 5 test1");
  15. }
  16. @Test
  17. void test2() {
  18. System.out.println("JUnit 5 test2");
  19. }
  20. @AfterEach
  21. void afterEachTest() {
  22. System.out.println("我是AfterEach,执行用例之后再执行我");
  23. }
  24. @AfterAll
  25. static void afterA11() {
  26. System.out.println("我是@AfterA11,测试类执行完了要执行我");
  27. }
  28. }

下面是运行结果

我是@BeforeAl1,测试类执行前要先执行我

我是@BeforeEach,执行用例前先执行我
JUnit 5 test1
我是AfterEach,执行用例之后再执行我


我是@BeforeEach,执行用例前先执行我
JUnit 5 test2
我是AfterEach,执行用例之后再执行我

我是@AfterA11,测试类执行完了要执行我

2. 禁用测试方法

测试用例 test1上加入注解@Disabled

    @Test
    @Disabled
    void test1() {
        System.out.println("junit5 test1");
    }

下面是测试结果

我是@BeforeAl1,测试类执行前要先执行我

void com.longser.union.cloud.tutorials.TestJunit5demo.test1() is @Disabled

我是@BeforeEach,执行用例前先执行我
JUnit 5 test2
我是AfterEach,执行用例之后再执行我

我是@AfterA11,测试类执行完了要执行我

从测试结果中我们可以看到test1用例被ignore,没有被执行

3. 设置展示名称

@DisplayName 注解可以给测试用例加上展示名称

    @Test
    @Disabled
    @DisplayName("测试用例1")
    void test1() {
        System.out.println("JUnit 5 test1");
    }

    @Test
    @DisplayName("测试用例2")
    void test2() {
        System.out.println("JUnit 5 test2");
    }

再次运行测试的时候,会把方法名改成展示名
image.png

4. 重复执行方法

对测试用例2加上注解@RepeatedTest,使其额外重复执行3次

    @Test
    @DisplayName("测试用例2")
    @RepeatedTest(3)
    void test2() {
        System.out.println("JUnit 5 test2");
    }

image.png
从测试结果中我们可以看到测试用例2被额外重复执行了3次

5. 嵌套测试

JUnit5提供了嵌套单元测试的功能,可以更好展示测试类之间的业务逻辑关系,我们通常是一个业务对应一个测试类,有业务关系的类其实可以写在一起。这样有利于进行测试。而且内联的写法可以大大减少不必要的类,精简项目,防止类爆炸等一系列问题。对于 @Nested 嵌套执行举例如下:

package com.longser.union.cloud.tutorials;

import org.junit.jupiter.api.*;

public class TestJunit5demo {
    @Test
    @DisplayName("外层测试")
    void outerTest() {
        System.out.println("最外层测试输出");
    }

    @Nested
    @DisplayName("内层测试1")
    class Inner {
        @Test
        void innerTest() {
            System.out.println("内层测试1输出");
        }

        @Nested
        @DisplayName("内层测试1嵌套")
        class InInnerl {
            @Test
            void InInnerTest() {
                System.out.println("内层测试1嵌套内层输出");
            }
        }
    }

    @Nested
    @DisplayName("内层测试2")
    class Inner2 {
        @Test
        void testInner2() {
            System.out.println("内层测试2输出");
        }
    }
}

image.png
由测试结果可以看出,@Nested 的执行顺序为先执行嵌套外层的用例,再以倒叙形式执行 @Nested 用例,然后再执行第二层嵌套的用例:外层->倒叙嵌套->第二层嵌套

6. 参数化测试

参数化测试可以按照多个参数分别运行多次单元测试这里有点类似于重复性测试,只不过每次运行传入的参数不用。需要使用到@ParameterizedTest,同时也需要@ValueSource 提供一组数据,它支持八种基本类型以及String和自定义对象类型,使用极其方便。

package com.longser.union.cloud.tutorials;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.junit.jupiter.params.provider.ValueSource;

public class TestJunit5Parameterized {
    @ParameterizedTest
    @ValueSource(ints = {1, 2, 3})
    @DisplayName("参数化测试")
    void paramTest(int a) {
        System.out.println("The parameter is " + a);
        Assertions.assertTrue(a > 0 && a < 4);
    }

    @ParameterizedTest(name = "{0} + {1} = {2}")
    @CsvSource({
            "0,    1,   1",
            "1,    2,   3",
            "49,  51, 100",
            "1,  100, 101"
    })
    void add(int first, int second, int expectedResult) {
        Assertions.assertEquals(expectedResult, first + second,
                () -> first + " + " + second + " should equal " + expectedResult);
    }
}

下面是测试的结果
image.png

14.4.4 Asssert(断言)

JUnit Jupiter 提供了强大的断言方法用以验证结果,在使用时需要借助java8的新特性lambda表达式,均是来自org.junit.jupiter.api.Assertions包的static方法( import static org.junit.jupiter.api.Assertions.* )包括assertTrue、assertFalse、assertNull、assertNotNull、assertEquals、assertArrayEquals、assertIterableEquals、assertLinesMatch、assertNotEquals、assertSame、assertNotSame、assertAll、assertThrows、assertDoesNotThrow、assertTimeout等。

assertTrue与assertFalse用来判断条件是否为true或false

@Test
@DisplayName("测试断言equals")
void testEquals() {
    assertTrue(3 < 4);
}

assertNull与assertNotNull用来判断条件是否为·null

@Test
@DisplayName("测试断言NotNull")
void testNotNull() {
    assertNotNull(new Object());
}

assertThrows用来判断执行抛出的异常是否符合预期,并可以使用异常类型接收返回值进行其他操作

@Test
@DisplayName("测试断言抛异常")
void testThrows() {
    ArithmeticException arithExcep = assertThrows(ArithmeticException.class, () -> {
        int m = 5/0;
    });
    assertEquals("/ by zero", arithExcep.getMessage());
}

assertTimeout用来判断执行过程是否超时

@Test
@DisplayName("测试断言超时")
void testTimeOut() {
    String actualResult = assertTimeout(ofSeconds(2), () -> {
        Thread.sleep(1000);
        return "a result";
    });
    System.out.println(actualResult);
}

assertAll是组合断言,当它内部所有断言正确执行完才算通过

    @Test
    @DisplayName("测试组合断言")
    void testAll() {
        assertAll("测试item商品下单",
                () -> {
                    //模拟用户余额扣减
                    assertTrue(1 < 2, "余额不足");
                },
                () -> {
                    //模拟item数据库扣减库存
                    assertTrue(3 < 4);
                },
                () -> {
                    //模拟交易流水落库
                    assertNotNull(new Object());
                }
        );
    }

14.5 第三方断言类库

虽然JUnit Jupiter提供的断言功能足以满足许多测试场景的需要,但是有时需要更强大和附加功能,例如匹配器。在这种情况下,JUnit小组推荐使用AssertJHamcrestTruth等第三方断言库。开发人员可以自由使用他们选择的断言库。

例如,匹配器和fluent API的组合可以用来使断言更具描述性和可读性。但是,JUnit Jupiter 的 Assertions 类没有提供类似于 JUnit 4 的assertThat() 方法,以接受 Hamcrest Matcher。相反,鼓励开发人员使用由第三方断言库提供的匹配器的内置支持。

以下示例演示如何在 JUnit Jupiter 测试中使用来自 Hamcrest 的 assertThat()支持。 只要 Hamcrest 库已经添加到 classpath 中,就可以静态地导入诸如 assertThat()、is() 和 equalTo() 之类的方法,然后像下面那样在测试中使用它们。

import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;

import org.junit.jupiter.api.Test;

class HamcrestAssertionDemo {

    @Test
    void assertWithHamcrestMatcher() {
        assertThat(2 + 1, is(equalTo(3)));
    }
}

14.6 参考资料

[

](https://doczhcn.gitbook.io/junit5/)

版权说明:本文由北京朗思云网科技股份有限公司原创,向互联网开放全部内容但保留所有权力。

[

](https://doczhcn.gitbook.io/junit5/)