程序员scholar 程序员scholar
首页
  • Java 基础

    • JavaSE
    • JavaIO
    • JavaAPI速查
  • Java 高级

    • JUC
    • JVM
    • Java新特性
    • 设计模式
  • Web 开发

    • Servlet
    • Java网络编程
  • 数据结构
  • HTTP协议
  • HTTPS协议
  • 计算机网络
  • Linux常用命令
  • Windows常用命令
  • SQL数据库

    • MySQL
    • MySQL速查
  • NoSQL数据库

    • Redis
    • ElasticSearch
  • 数据库

    • MyBatis
    • MyBatis-Plus
  • 消息中间件

    • RabbitMQ
  • 服务器

    • Nginx
  • Python 基础

    • Python基础
  • Python 进阶

    • 装饰器与生成器
    • 异常处理
    • 标准库精讲
    • 模块与包
    • pip包管理工具
  • Spring框架

    • Spring6
    • SpringMVC
    • SpringBoot
    • SpringSecurity
  • SpringCould微服务

    • SpringCloud基础
    • 微服务之DDD架构思想
  • 日常必备

    • 开发常用工具包
    • Hutoll工具包
    • IDEA常用配置
    • 开发笔记
    • 日常记录
    • 项目部署
    • 网站导航
    • 产品学习
    • 英语学习
  • 代码管理

    • Maven
    • Git教程
    • Git小乌龟教程
  • 运维工具

    • Docker
    • Jenkins
    • Kubernetes
前端 (opens new window)
  • 算法笔记

    • 算法思想
    • 刷题笔记
  • 面试问题常见

    • 十大经典排序算法
    • 面试常见问题集锦
关于
GitHub (opens new window)
首页
  • Java 基础

    • JavaSE
    • JavaIO
    • JavaAPI速查
  • Java 高级

    • JUC
    • JVM
    • Java新特性
    • 设计模式
  • Web 开发

    • Servlet
    • Java网络编程
  • 数据结构
  • HTTP协议
  • HTTPS协议
  • 计算机网络
  • Linux常用命令
  • Windows常用命令
  • SQL数据库

    • MySQL
    • MySQL速查
  • NoSQL数据库

    • Redis
    • ElasticSearch
  • 数据库

    • MyBatis
    • MyBatis-Plus
  • 消息中间件

    • RabbitMQ
  • 服务器

    • Nginx
  • Python 基础

    • Python基础
  • Python 进阶

    • 装饰器与生成器
    • 异常处理
    • 标准库精讲
    • 模块与包
    • pip包管理工具
  • Spring框架

    • Spring6
    • SpringMVC
    • SpringBoot
    • SpringSecurity
  • SpringCould微服务

    • SpringCloud基础
    • 微服务之DDD架构思想
  • 日常必备

    • 开发常用工具包
    • Hutoll工具包
    • IDEA常用配置
    • 开发笔记
    • 日常记录
    • 项目部署
    • 网站导航
    • 产品学习
    • 英语学习
  • 代码管理

    • Maven
    • Git教程
    • Git小乌龟教程
  • 运维工具

    • Docker
    • Jenkins
    • Kubernetes
前端 (opens new window)
  • 算法笔记

    • 算法思想
    • 刷题笔记
  • 面试问题常见

    • 十大经典排序算法
    • 面试常见问题集锦
关于
GitHub (opens new window)
npm

(进入注册为作者充电)

  • Spring Boot

    • Spring Boot - 自动配置
    • Spring Boot - 自定义starter
    • Spring Boot - 配置文件
    • Spring Boot - 自定义SpringApplication
    • Spring Boot - 生命周期与事件
    • Spring Boot - 事件驱动
    • Spring Boot - Bean 加载方式
    • Spring Boot - 容器资源感知与获取
    • Spring Boot - 定时任务
    • Spring Boot - 异步任务
    • Spring Boot - 内置日志
    • Spring Boot - 函数式 Web
    • Spring Boot - 响应式远程调用
    • Spring Boot - 接口文档
    • Spring Boot - 单元测试
      • 1. JUnit 5架构概述与核心组件
      • 2. 在Spring Boot中配置和使用JUnit 5
        • 添加依赖
        • 创建第一个测试类
        • Spring Boot测试的关键特性
      • 3. 使用@Transactional实现测试事务与自动回滚
      • 4. JUnit 5常用注解
        • 核心测试注解
        • 测试生命周期注解
        • 其他辅助注解
        • 完整使用案例
      • 5. JUnit 5断言机制:验证测试结果
      • 6. 数组与集合断言:比较复杂数据结构
      • 7. 异常断言:验证异常抛出情况
      • 8. 超时断言:性能测试与超时监控
      • 9. 快速失败:主动中断测试
      • 10. 前置条件:有条件地执行测试
      • 11. 嵌套测试:组织相关测试场景
      • 12. 参数化测试:多参数测试减少代码冗余
    • Spring Boot - 内容协商
    • Spring Boot - 参数校验
    • Spring Boot - HTTP客户端工具
    • Spring Boot - 控制器请求映射
    • Spring Boot - 请求参数接收
    • Spring Boot - 通用响应类
    • Spring Boot - 全局异常处理
    • Spring Boot - 整合Druid
    • Spring Boot - 整合Thymeleaf
    • Spring Boot - 国际化实现
    • Spring Boot - 自定义注解
  • Spring高级
  • Spring Boot
scholar
2023-11-01
目录

Spring Boot - 单元测试

# Spring Boot - 单元测试

# 1. JUnit 5架构概述与核心组件

Spring Boot 2.2.0版本开始正式引入JUnit 5作为单元测试的默认库。作为一个全新设计的测试框架,JUnit 5相比于之前的版本有了显著变化,采用了模块化的架构设计。JUnit 5由三个主要子项目组成:

JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage

  • JUnit Platform:作为JVM上测试框架的基础平台,提供了启动测试引擎的API。它不仅支持JUnit自己的测试引擎,还可以集成其他第三方测试引擎,如TestNG等。

  • JUnit Jupiter:包含JUnit 5的全新编程模型和扩展机制,是JUnit 5新特性的核心实现。它内部包含了一个测试引擎,用于在JUnit Platform上执行基于Jupiter API编写的测试。

  • JUnit Vintage:为了向后兼容JUnit 3和JUnit 4而设计,允许在JUnit 5平台上运行旧版本的测试用例。

重要版本说明

注意:Spring Boot 2.4及以上版本默认不再包含Vintage模块,这意味着不能直接使用JUnit 4的注解(如JUnit 4的@Test)。如果需要兼容JUnit 4的测试代码,需要手动添加以下依赖:

<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>
1
2
3
4
5
6
7
8
9
10
11

# 2. 在Spring Boot中配置和使用JUnit 5

# 添加依赖

在Spring Boot项目中使用JUnit 5非常简单,只需在pom.xml中添加以下依赖:

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

spring-boot-starter-test是一个综合测试启动器,它包含了:

  • JUnit 5
  • Spring Test与Spring Boot Test
  • AssertJ, Hamcrest等断言库
  • Mockito用于创建和配置mock对象
  • JSONassert用于JSON断言
  • JsonPath用于XPath风格访问JSON

# 创建第一个测试类

import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

/**
 * 基础测试类,用于验证Spring应用上下文是否能够正常加载
 * @SpringBootTest注解会创建应用程序上下文并加载完整的Spring应用
 */
@SpringBootTest
class ApplicationTests {

    /**
     * 简单的上下文加载测试,验证应用是否能够正常启动
     * 如果Spring上下文无法创建,此测试将失败
     */
    @Test
    void contextLoads() {
        // 仅测试Spring上下文是否成功加载
        // 不需要任何断言,上下文加载失败会导致测试失败
    }
    
    /**
     * 自定义测试方法示例
     */
    @Test
    void simpleTest() {
        // 输出消息,表明测试正在执行
        System.out.println("执行测试方法...");
        // 这里可以添加实际的测试逻辑和断言
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

# Spring Boot测试的关键特性

Spring Boot测试提供了多种特性:

  1. 完整的Spring上下文:@SpringBootTest注解会创建完整的Spring应用上下文
  2. 依赖注入支持:可以使用@Autowired注入任何Spring管理的Bean
  3. 事务支持:可以使用@Transactional使测试自动回滚
  4. 测试配置:可以使用@TestPropertySource或properties属性覆盖配置

# 3. 使用@Transactional实现测试事务与自动回滚

在测试数据库操作时,通常不希望测试数据永久保存到数据库中。Spring提供了简单的方式使测试在执行后自动回滚:

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.transaction.annotation.Transactional;

/**
 * 演示在测试中使用事务自动回滚的示例
 */
@SpringBootTest
class UserServiceTests {

    @Autowired
    private UserRepository userRepository; // 注入用户数据访问组件
    
    @Autowired
    private UserService userService; // 注入用户服务组件
    
    /**
     * 使用@Transactional注解使测试自动回滚
     * 无论测试是否通过,测试中对数据库的所有操作都会被回滚
     */
    @Test
    @Transactional
    void testCreateUser() {
        // 测试前检查初始用户数量
        long initialCount = userRepository.count();
        
        // 执行创建用户操作
        User newUser = new User("testuser", "test@example.com");
        userService.createUser(newUser);
        
        // 验证用户已被创建
        assert userRepository.count() == initialCount + 1;
        
        // 测试完成后,@Transactional会自动回滚,
        // 数据库中不会保留此测试创建的用户
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

关键说明:

  • @Transactional注解应用在测试方法上时,会在测试完成后自动回滚所有数据库操作
  • 这确保了测试的隔离性和可重复性,每次测试都从相同的数据库状态开始
  • 只有在使用Spring的事务管理器和支持事务的数据库时,此功能才有效
  • 如果需要保留测试数据,可以使用@Rollback(false)注解覆盖默认行为

# 4. JUnit 5常用注解

JUnit 5提供了丰富的注解集,显著增强了测试的表达能力和功能性。与JUnit 4相比,很多注解的名称和功能都有所变化。

# 核心测试注解

注解 描述
@Test 标识一个测试方法。注意:与JUnit 4不同,该注解不能包含属性参数
@ParameterizedTest 标识一个参数化测试方法,允许使用不同参数多次运行
@RepeatedTest 标识一个需要重复执行的测试方法
@DisplayName 自定义测试类或测试方法的显示名称,改善测试报告的可读性
@Disabled 禁用测试类或测试方法,等同于JUnit 4中的@Ignore
@Timeout 指定测试方法的执行超时时间

# 测试生命周期注解

注解 描述
@BeforeEach 标识在每个测试方法执行前都要运行的方法
@AfterEach 标识在每个测试方法执行后都要运行的方法
@BeforeAll 标识在所有测试方法执行前运行一次的静态方法
@AfterAll 标识在所有测试方法执行后运行一次的静态方法

# 其他辅助注解

注解 描述
@Tag 标记测试类或方法,用于过滤执行(类似JUnit 4的@Category)
@ExtendWith 声明使用的扩展类,用于自定义测试行为
@TestMethodOrder 控制测试方法的执行顺序

# 完整使用案例

import org.junit.jupiter.api.*;
import org.springframework.boot.test.context.SpringBootTest;

/**
 * 演示JUnit 5各种注解的完整使用示例
 * @DisplayName注解用于自定义测试类的显示名称
 */
@SpringBootTest
@DisplayName("用户服务功能测试套件")
public class UserServiceAnnotationDemo {
    
    /**
     * 在所有测试执行前运行一次,必须是静态方法
     * 通常用于全局设置、资源初始化等
     */
    @BeforeAll
    public static void setupTestSuite() {
        System.out.println("==== 测试套件初始化 ====");
        // 进行全局初始化,例如创建共享资源
    }
    
    /**
     * 在每个测试方法执行前运行
     * 用于为每个测试准备测试环境
     */
    @BeforeEach
    public void setupEachTest() {
        System.out.println("-- 单个测试开始前的准备工作 --");
        // 为每个测试方法准备环境,例如创建测试数据
    }
    
    /**
     * 基本测试方法
     */
    @Test
    @DisplayName("验证用户创建功能")
    public void testCreateUser() {
        System.out.println("执行用户创建测试");
        // 测试逻辑...
    }
    
    /**
     * 使用@Disabled注解禁用的测试方法
     * 该方法不会在正常测试执行中运行
     */
    @Test
    @Disabled("此功能尚未实现,暂时禁用测试")
    @DisplayName("验证用户更新功能")
    public void testUpdateUser() {
        System.out.println("此测试不会执行");
        // 测试逻辑...
    }
    
    /**
     * 设置了超时时间的测试方法
     * 如果执行时间超过500毫秒,测试将失败
     */
    @Test
    @Timeout(value = 500, unit = TimeUnit.MILLISECONDS)
    @DisplayName("验证用户查询性能")
    public void testUserQueryPerformance() {
        System.out.println("执行性能测试");
        // 测试逻辑...
    }
    
    /**
     * 在每个测试方法执行后运行
     * 用于清理测试资源
     */
    @AfterEach
    public void cleanupEachTest() {
        System.out.println("-- 单个测试结束后的清理工作 --");
        // 清理测试资源,例如删除测试数据
    }
    
    /**
     * 在所有测试执行完成后运行一次,必须是静态方法
     * 通常用于全局资源释放
     */
    @AfterAll
    public static void cleanupTestSuite() {
        System.out.println("==== 测试套件清理 ====");
        // 清理全局资源
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85

执行结果示例:

==== 测试套件初始化 ====
-- 单个测试开始前的准备工作 --
执行用户创建测试
-- 单个测试结束后的清理工作 --
-- 单个测试开始前的准备工作 --
执行性能测试
-- 单个测试结束后的清理工作 --
==== 测试套件清理 ====
1
2
3
4
5
6
7
8

重要说明:

  • @BeforeAll和@AfterAll方法必须是静态(static)方法,因为它们在测试实例创建前/后执行
  • @Disabled可用于暂时排除不需要执行的测试,应当提供禁用原因
  • 测试方法的执行顺序默认是不确定的,可以使用@TestMethodOrder控制顺序

# 5. JUnit 5断言机制:验证测试结果

断言是测试的核心部分,用于验证测试结果是否符合预期。JUnit 5提供了丰富的断言API,都位于org.junit.jupiter.api.Assertions类中。

基本断言方法

以下是JUnit 5中最常用的断言方法:

import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

/**
 * 展示JUnit 5基本断言方法的使用
 */
@DisplayName("基本断言示例")
public class BasicAssertionsDemo {

    /**
     * 演示各种相等性断言
     */
    @Test
    @DisplayName("相等性断言测试")
    public void testEqualityAssertions() {
        // 测试两个基本类型值是否相等
        assertEquals(4, 2 + 2, "2+2应该等于4");
        
        // 使用delta比较浮点数
        assertEquals(0.33, 1/3.0, 0.01, "浮点数比较需要使用误差范围");
        
        // 测试两个对象是否不相等
        assertNotEquals("预期值", "实际值", "两个字符串不应相等");
        
        // 测试两个对象引用是否指向同一对象
        Object obj = new Object();
        Object sameObj = obj;
        assertSame(obj, sameObj, "应该是同一个对象引用");
        
        // 测试两个对象引用是否指向不同对象
        Object anotherObj = new Object();
        assertNotSame(obj, anotherObj, "应该是不同的对象引用");
    }
    
    /**
     * 演示布尔值断言
     */
    @Test
    @DisplayName("布尔值断言测试")
    public void testBooleanAssertions() {
        // 测试条件是否为true
        assertTrue(5 > 3, "5应该大于3");
        
        // 测试条件是否为false
        assertFalse(5 < 3, "5不应该小于3");
    }
    
    /**
     * 演示空值断言
     */
    @Test
    @DisplayName("空值断言测试")
    public void testNullAssertions() {
        // 测试对象是否为null
        String nullString = null;
        assertNull(nullString, "字符串应该为null");
        
        // 测试对象是否不为null
        String notNullString = "Hello";
        assertNotNull(notNullString, "字符串不应该为null");
    }
    
    /**
     * 带自定义错误消息的断言
     * 当断言失败时,会显示这些消息
     */
    @Test
    @DisplayName("自定义错误消息断言")
    public void testAssertionsWithMessages() {
        // 使用静态错误消息
        assertEquals(5, 2 + 3, "计算错误:2+3应该等于5");
        
        // 使用延迟计算的错误消息(仅在断言失败时创建)
        // 当有复杂的错误消息需要构建时,这种方式更高效
        assertEquals(10, 5 * 2, () -> "计算错误:期望值是10,但实际值是" + (5 * 2));
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78

# 6. 数组与集合断言:比较复杂数据结构

JUnit 5提供了专门用于数组和集合比较的断言方法,大大简化了对复杂数据结构的验证。

import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.util.Arrays;
import java.util.List;

/**
 * 展示数组和集合断言的使用
 */
@DisplayName("数组和集合断言测试")
public class ArrayAndCollectionAssertionsDemo {

    /**
     * 测试数组相等性
     */
    @Test
    @DisplayName("数组相等性测试")
    public void testArrayEquals() {
        // 测试基本类型数组
        int[] expected = {1, 2, 3};
        int[] actual = {1, 2, 3};
        assertArrayEquals(expected, actual, "数组应该完全相等");
        
        // 测试对象数组
        String[] expectedStrings = {"apple", "banana", "cherry"};
        String[] actualStrings = {"apple", "banana", "cherry"};
        assertArrayEquals(expectedStrings, actualStrings, "字符串数组应该相等");
        
        // 注意:数组比较要求元素顺序完全一致
    }
    
    /**
     * 测试多维数组相等性
     */
    @Test
    @DisplayName("多维数组相等性测试")
    public void testMultidimensionalArrayEquals() {
        // 测试二维数组
        int[][] expected2D = {{1, 2}, {3, 4}};
        int[][] actual2D = {{1, 2}, {3, 4}};
        assertArrayEquals(expected2D, actual2D, "二维数组应该相等");
    }
    
    /**
     * 测试集合内容
     */
    @Test
    @DisplayName("集合内容测试")
    public void testCollectionContents() {
        // 使用assertEquals测试List相等性
        List<String> expectedList = Arrays.asList("apple", "banana", "cherry");
        List<String> actualList = Arrays.asList("apple", "banana", "cherry");
        assertEquals(expectedList, actualList, "列表内容应该相等");
        
        // 验证集合大小
        assertEquals(3, actualList.size(), "列表应该包含3个元素");
        
        // 验证集合包含特定元素
        assertTrue(actualList.contains("banana"), "列表应该包含'banana'");
    }
    
    /**
     * 使用组合断言测试多个条件
     */
    @Test
    @DisplayName("组合断言测试")
    public void testWithAssertAll() {
        // assertAll允许在单个测试中分组多个断言
        // 即使其中一个断言失败,其他断言仍会执行
        int[] numbers = {1, 2, 3, 4, 5};
        List<String> fruits = Arrays.asList("apple", "banana", "cherry");
        
        assertAll("数组和集合验证",
            // 数组断言
            () -> assertEquals(5, numbers.length, "数组长度应为5"),
            () -> assertEquals(3, numbers[2], "第三个元素应为3"),
            
            // 集合断言
            () -> assertEquals(3, fruits.size(), "水果列表应有3项"),
            () -> assertTrue(fruits.contains("banana"), "应包含banana"),
            () -> assertFalse(fruits.contains("mango"), "不应包含mango")
        );
        
        // 如果任何断言失败,错误报告会包含所有失败的断言
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86

关键说明:

  • assertArrayEquals方法专门用于比较数组元素,会考虑数组类型、长度和元素顺序
  • assertEquals可用于比较实现了equals方法的集合类
  • assertAll组合多个断言,即使其中一个失败,其他断言仍会执行,非常适合复杂数据结构的验证

# 7. 异常断言:验证异常抛出情况

在测试中,验证代码是否正确抛出预期异常也是很重要的。JUnit 5提供了简洁的API来测试异常情况。

import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.io.IOException;

/**
 * 展示如何测试方法是否正确抛出异常
 */
@DisplayName("异常测试示例")
public class ExceptionAssertionsDemo {

    /**
     * 测试是否抛出特定类型的异常
     */
    @Test
    @DisplayName("验证方法抛出异常")
    public void testExceptionThrown() {
        // 测试代码是否抛出预期的异常类型
        Exception exception = assertThrows(
            // 预期的异常类型
            IllegalArgumentException.class,
            // 可能抛出异常的代码块
            () -> {
                throw new IllegalArgumentException("参数无效");
            },
            // 断言失败时的错误消息
            "应该抛出IllegalArgumentException"
        );
        
        // 可以进一步验证异常消息
        assertEquals("参数无效", exception.getMessage(), 
                   "异常消息应该匹配");
    }
    
    /**
     * 测试更复杂的异常情况
     */
    @Test
    @DisplayName("测试更复杂的异常场景")
    public void testComplexExceptionScenario() {
        // 使用Service类的方法进行测试(假设的Service类)
        UserService service = new UserService();
        
        // 验证特定场景是否抛出预期异常
        Exception exception = assertThrows(
            UserNotFoundException.class,
            () -> service.getUserById(999L), // 假定999是不存在的ID
            "查询不存在的用户ID应该抛出UserNotFoundException"
        );
        
        // 验证异常包含用户ID信息
        assertTrue(exception.getMessage().contains("999"), 
                 "异常消息应该包含用户ID");
    }
    
    /**
     * 测试代码不抛出异常
     */
    @Test
    @DisplayName("验证方法不抛出异常")
    public void testNoExceptionThrown() {
        // 断言代码块不会抛出任何异常
        assertDoesNotThrow(
            // 不应抛出异常的代码块
            () -> {
                // 一些不应该抛出异常的操作
                int result = 1 + 1;
            },
            "此代码不应抛出异常"
        );
    }
    
    /**
     * 测试受检异常(checked exception)的处理
     */
    @Test
    @DisplayName("验证受检异常抛出")
    public void testCheckedExceptionThrown() {
        // 测试IO异常抛出
        assertThrows(
            IOException.class,
            () -> {
                // 一些可能抛出IOException的代码
                throw new IOException("文件读取失败");
            },
            "应该抛出IOException"
        );
    }
    
    // 模拟的用户服务类
    class UserService {
        public User getUserById(Long id) {
            if (id == 999L) {
                throw new UserNotFoundException("用户ID为" + id + "的用户不存在");
            }
            return new User("user" + id);
        }
    }
    
    // 模拟的用户类
    class User {
        private String name;
        
        public User(String name) {
            this.name = name;
        }
    }
    
    // 模拟的自定义异常
    class UserNotFoundException extends RuntimeException {
        public UserNotFoundException(String message) {
            super(message);
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115

关键说明:

  • assertThrows断言代码会抛出特定类型的异常
  • 可以捕获异常对象进行进一步验证,如检查异常消息
  • assertDoesNotThrow断言代码不会抛出任何异常
  • 对于测试业务逻辑的正确性,异常测试与正常流程测试同样重要

# 8. 超时断言:性能测试与超时监控

对于性能敏感的应用,测试方法的执行时间是很重要的。JUnit 5提供了超时断言来确保方法在指定时间内完成。

import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.time.Duration;
import java.util.concurrent.TimeUnit;

/**
 * 演示超时断言的使用
 */
@DisplayName("超时测试示例")
public class TimeoutAssertionsDemo {

    /**
     * 测试方法执行不超过指定时间
     */
    @Test
    @DisplayName("验证方法执行时间")
    public void testMethodExecutionTime() {
        // 断言代码执行时间不超过500毫秒
        assertTimeout(
            // 最大允许执行时间
            Duration.ofMillis(500),
            // 被测试的代码块
            () -> {
                // 执行一些操作,模拟耗时任务
                Thread.sleep(100);  // 模拟耗时100毫秒的操作
            },
            // 超时错误消息
            "方法执行时间不应超过500毫秒"
        );
    }
    
    /**
     * 测试方法执行不超过指定时间,并返回结果
     */
    @Test
    @DisplayName("带返回值的超时测试")
    public void testTimeoutWithResult() {
        // 断言代码执行时间不超过1秒,并获取执行结果
        String result = assertTimeout(
            Duration.ofSeconds(1),
            () -> {
                // 执行耗时操作并返回结果
                Thread.sleep(500);
                return "操作完成";
            }
        );
        
        // 验证返回结果
        assertEquals("操作完成", result, "返回值应该是'操作完成'");
    }
    
    /**
     * 使用提前终止的超时测试
     * 如果执行时间超过限制,会立即中断执行
     */
    @Test
    @DisplayName("提前终止的超时测试")
    public void testTimeoutPreemptively() {
        // 断言代码执行时间不超过500毫秒,一旦超时立即中断
        assertTimeoutPreemptively(
            Duration.ofMillis(500),
            () -> {
                // 模拟长时间运行的任务
                Thread.sleep(100);
            },
            "方法执行时间不应超过500毫秒"
        );
        
        // 注意:assertTimeoutPreemptively会在单独的线程中执行代码
        // 如果代码使用了ThreadLocal变量或事务,可能会导致问题
    }
    
    /**
     * 使用@Timeout注解标记整个测试方法的超时时间
     */
    @Test
    @Timeout(value = 1, unit = TimeUnit.SECONDS)
    @DisplayName("使用@Timeout注解的测试")
    public void testWithTimeoutAnnotation() {
        // 此测试必须在1秒内完成,否则将失败
        try {
            Thread.sleep(500);  // 模拟耗时500毫秒的操作
        } catch (InterruptedException e) {
            fail("测试被中断");
        }
    }
    
    /**
     * 测试数据库查询性能(模拟示例)
     */
    @Test
    @DisplayName("数据库查询性能测试")
    public void testDatabaseQueryPerformance() {
        // 断言数据库查询时间不超过200毫秒
        assertTimeout(
            Duration.ofMillis(200),
            () -> {
                // 模拟数据库查询
                simulateDatabaseQuery();
            },
            "数据库查询应在200毫秒内完成"
        );
    }
    
    // 模拟数据库查询的辅助方法
    private void simulateDatabaseQuery() throws InterruptedException {
        // 模拟数据库操作耗时
        Thread.sleep(50);  // 假设查询需要50毫秒
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111

关键说明:

  • assertTimeout断言代码在特定时间内执行完成,但会等待代码执行结束
  • assertTimeoutPreemptively一旦超时就会立即中断执行,适用于严格的性能测试
  • @Timeout注解可以直接应用于测试方法,简化超时测试的编写
  • 谨慎使用assertTimeoutPreemptively,因为它在单独线程中执行代码,可能影响ThreadLocal和事务
  • 超时断言对于测试性能瓶颈和保证代码效率非常有用

# 9. 快速失败:主动中断测试

有时候在测试过程中,如果发现某些条件不满足,无需继续执行测试,可以使用fail方法主动使测试失败:

import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

/**
 * 演示快速失败功能的使用
 */
@DisplayName("快速失败测试示例")
public class FailingTestDemo {

    /**
     * 使用fail方法直接使测试失败
     */
    @Test
    @DisplayName("直接失败的测试")
    public void testDirectFail() {
        // 直接使测试失败,并提供错误消息
        fail("此测试故意失败");
    }
    
    /**
     * 在条件判断中使用fail方法
     */
    @Test
    @DisplayName("条件失败测试")
    public void testConditionalFail() {
        int userAge = 15;
        
        // 如果年龄小于18,使测试失败
        if (userAge < 18) {
            fail("用户年龄(" + userAge + ")不满足最小要求(18)");
        }
        
        // 正常的测试流程...
    }
    
    /**
     * 在异常捕获中使用fail方法
     */
    @Test
    @DisplayName("异常处理中的失败测试")
    public void testExceptionHandlingFail() {
        try {
            // 执行一些可能抛出异常的代码
            methodThatShouldThrowException();
            
            // 如果上面的方法没有抛出异常,则测试应该失败
            fail("预期应抛出异常,但没有抛出");
        } catch (Exception e) {
            // 验证异常类型
            assertTrue(e instanceof IllegalStateException, 
                      "应该抛出IllegalStateException");
        }
    }
    
    /**
     * 测试未实现的功能
     */
    @Test
    @DisplayName("未实现功能测试")
    public void testNotYetImplementedFeature() {
        // 标记测试为未实现
        fail("此功能尚未实现");
        
        // 下面的代码不会执行
    }
    
    // 模拟一个应该抛出异常的方法
    private void methodThatShouldThrowException() {
        throw new IllegalStateException("预期的异常");
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72

关键说明:

  • fail方法直接使测试失败,可以提供自定义错误消息
  • 可以用在条件判断中,提前结束不满足条件的测试
  • 用于标记未实现的功能或未达到预期的情况
  • 虽然大多数情况应使用标准断言,但fail方法在特定场景下很有用
  • 在异常测试中,assertThrows通常比try-catch+fail更简洁

# 10. 前置条件:有条件地执行测试

JUnit 5引入了前置条件(assumptions)概念,允许在特定条件满足时才执行测试。与断言不同,前置条件不满足时会跳过测试而不是标记为失败。

import static org.junit.jupiter.api.Assertions.*;
import static org.junit.jupiter.api.Assumptions.*;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

/**
 * 演示前置条件的使用
 */
@DisplayName("前置条件测试示例")
public class AssumptionsDemo {

    /**
     * 使用assumeTrue验证条件
     * 如果条件为false,测试将被跳过
     */
    @Test
    @DisplayName("根据操作系统执行测试")
    public void testOnlyOnWindowsOS() {
        // 检查是否是Windows操作系统
        String osName = System.getProperty("os.name");
        
        // 假设操作系统是Windows
        assumeTrue(
            osName.toLowerCase().contains("windows"),
            "此测试仅在Windows上运行"
        );
        
        // 如果是Windows,测试会继续执行
        // 否则测试会被跳过,标记为"已禁用"而非"失败"
        System.out.println("在Windows系统上运行的测试");
    }
    
    /**
     * 使用assumeFalse验证条件
     * 如果条件为true,测试将被跳过
     */
    @Test
    @DisplayName("根据环境变量跳过测试")
    public void testNotInCiEnvironment() {
        // 获取CI环境标志
        boolean isCiEnvironment = "true".equals(System.getenv("CI"));
        
        // 假设不是CI环境
        assumeFalse(
            isCiEnvironment,
            "此测试不应在CI环境中运行"
        );
        
        // 如果不是CI环境,测试会继续
        // 否则测试会被跳过
        System.out.println("在本地开发环境中运行的测试");
    }
    
    /**
     * 使用assumingThat进行条件性测试
     * 只有在条件满足时才执行特定代码块
     */
    @Test
    @DisplayName("条件执行部分测试逻辑")
    public void testAssumingThat() {
        // 获取Java版本
        String javaVersion = System.getProperty("java.version");
        
        // 如果是Java 11,执行特定测试
        assumingThat(
            javaVersion.startsWith("11"),
            () -> {
                // 仅在Java 11上执行的代码
                System.out.println("在Java 11上运行的特定测试");
                assertEquals(11, 5 + 6, "Java 11特定测试");
            }
        );
        
        // 无论Java版本如何,以下代码都会执行
        System.out.println("在所有Java版本上运行的通用测试");
        assertEquals(2, 1 + 1, "通用测试");
    }
    
    /**
     * 组合多个前置条件
     */
    @Test
    @DisplayName("组合多个前置条件")
    public void testWithMultipleAssumptions() {
        // 检查多个条件
        String osName = System.getProperty("os.name");
        String javaVersion = System.getProperty("java.version");
        boolean isLocalEnv = "local".equals(System.getProperty("env.type"));
        
        // 以下三个条件必须全部满足,否则测试将被跳过
        assumeTrue(osName.contains("Linux"), "需要Linux系统");
        assumeTrue(javaVersion.startsWith("11"), "需要Java 11");
        assumeTrue(isLocalEnv, "需要本地开发环境");
        
        // 只有在满足所有条件时才会执行到这里
        System.out.println("在Linux + Java 11 + 本地环境上运行的测试");
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98

关键说明:

  • assumeTrue:当条件为true时继续测试,否则跳过
  • assumeFalse:当条件为false时继续测试,否则跳过
  • assumingThat:只有在条件为true时执行指定的代码块,其余测试照常运行
  • 前置条件不会导致测试失败,只会导致测试被跳过
  • 适用于环境敏感的测试,例如只在特定操作系统、特定Java版本或特定配置下运行

# 11. 嵌套测试:组织相关测试场景

JUnit 5通过@Nested注解支持嵌套测试类,可以更好地组织关联场景的测试,提高测试代码的结构性和可读性。

import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.*;

/**
 * 演示如何使用嵌套测试组织相关的测试场景
 */
@DisplayName("购物车功能测试")
public class ShoppingCartTest {

    // 测试中使用的购物车实例
    private ShoppingCart cart;
    
    /**
     * 每个测试方法前执行的设置
     */
    @BeforeEach
    void setUp() {
        // 创建新的购物车实例
        cart = new ShoppingCart();
        System.out.println("创建新的购物车");
    }
    
    /**
     * 测试新创建的空购物车
     */
    @Test
    @DisplayName("新购物车应为空")
    void testNewCartIsEmpty() {
        assertTrue(cart.isEmpty(), "新购物车应该是空的");
        assertEquals(0, cart.getItemCount(), "新购物车的商品数量应为0");
    }

    /**
     * 针对空购物车的测试场景
     */
    @Nested
    @DisplayName("当购物车是空的")
    class WhenEmpty {
        
        @BeforeEach
        void setUpEmptyCart() {
            // 确保购物车是空的
            cart.clear();
            System.out.println("准备空购物车测试");
        }
        
        @Test
        @DisplayName("添加商品后应不再为空")
        void testAddingItemToEmptyCart() {
            // 向空购物车添加商品
            cart.addItem(new Item("苹果", 5.0));
            
            // 验证购物车不再为空
            assertFalse(cart.isEmpty(), "添加商品后购物车不应为空");
            assertEquals(1, cart.getItemCount(), "购物车应包含1件商品");
        }
        
        @Test
        @DisplayName("空购物车总价应为0")
        void testEmptyCartTotalPrice() {
            assertEquals(0.0, cart.getTotalPrice(), "空购物车总价应为0");
        }
        
        @Test
        @DisplayName("从空购物车移除商品应抛出异常")
        void testRemovingItemFromEmptyCart() {
            // 验证从空购物车移除商品会抛出异常
            assertThrows(IllegalStateException.class, 
                       () -> cart.removeItem(1),
                       "从空购物车移除商品应抛出异常");
        }
        
        /**
         * 针对非空购物车的测试场景,嵌套在空购物车内
         * 展示嵌套测试的层次结构
         */
        @Nested
        @DisplayName("当添加了商品后")
        class WhenItemAdded {
            
            private Item testItem;
            
            @BeforeEach
            void addItemToCart() {
                testItem = new Item("香蕉", 4.0);
                cart.addItem(testItem);
                System.out.println("添加商品到购物车");
            }
            
            @Test
            @DisplayName("购物车应包含添加的商品")
            void testCartContainsAddedItem() {
                assertTrue(cart.containsItem(testItem), "购物车应包含添加的商品");
                assertEquals(1, cart.getItemCount(), "购物车应有1件商品");
            }
            
            @Test
            @DisplayName("购物车总价应等于商品价格")
            void testCartTotalPrice() {
                assertEquals(4.0, cart.getTotalPrice(), "购物车总价应等于商品价格");
            }
            
            @Nested
            @DisplayName("当添加多个商品后")
            class WhenMultipleItemsAdded {
                
                @BeforeEach
                void addMoreItems() {
                    cart.addItem(new Item("橙子", 3.0));
                    cart.addItem(new Item("葡萄", 6.0));
                    System.out.println("添加更多商品到购物车");
                }
                
                @Test
                @DisplayName("购物车应包含所有添加的商品")
                void testCartContainsAllAddedItems() {
                    assertEquals(3, cart.getItemCount(), "购物车应有3件商品");
                }
                
                @Test
                @DisplayName("购物车总价应等于所有商品价格之和")
                void testCartTotalPriceWithMultipleItems() {
                    assertEquals(13.0, cart.getTotalPrice(), "购物车总价应为13.0");
                }
                
                @Test
                @DisplayName("清空购物车后应为空")
                void testClearCart() {
                    cart.clear();
                    assertTrue(cart.isEmpty(), "清空后购物车应为空");
                    assertEquals(0, cart.getItemCount(), "清空后商品数量应为0");
                }
            }
        }
    }
    
    // 模拟的购物车类
    static class ShoppingCart {
        private java.util.List<Item> items = new java.util.ArrayList<>();
        
        public void addItem(Item item) {
            items.add(item);
        }
        
        public void removeItem(int index) {
            if (isEmpty()) {
                throw new IllegalStateException("购物车为空,无法移除商品");
            }
            items.remove(index);
        }
        
        public boolean containsItem(Item item) {
            return items.contains(item);
        }
        
        public int getItemCount() {
            return items.size();
        }
        
        public boolean isEmpty() {
            return items.isEmpty();
        }
        
        public double getTotalPrice() {
            return items.stream()
                      .mapToDouble(Item::getPrice)
                      .sum();
        }
        
        public void clear() {
            items.clear();
        }
    }
    
    // 模拟的商品类
    static class Item {
        private String name;
        private double price;
        
        public Item(String name, double price) {
            this.name = name;
            this.price = price;
        }
        
        public String getName() {
            return name;
        }
        
        public double getPrice() {
            return price;
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193

关键说明:

  • @Nested注解用于创建嵌套测试类,必须是非静态内部类
  • 嵌套测试可以更好地表达测试之间的关系,如"当...时"
  • 内层测试可以继承外层测试的上下文,更好地共享测试设置
  • 每个嵌套级别都可以有自己的@BeforeEach和@AfterEach方法
  • 嵌套测试在测试报告中以层次结构显示,使得测试结果更易理解
  • 嵌套测试特别适合状态依赖的测试场景,如空购物车→添加商品→结账

# 12. 参数化测试:多参数测试减少代码冗余

参数化测试是JUnit 5的一大特色,它允许使用不同的参数多次运行同一个测试方法,极大减少了测试代码冗余。

要使用参数化测试,首先需要添加junit-jupiter-params依赖(这通常包含在spring-boot-starter-test中):

<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-params</artifactId>
    <scope>test</scope>
</dependency>
1
2
3
4
5

参数化测试示例:

import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.*;
import java.util.stream.Stream;
import java.time.Month;
import java.util.EnumSet;

/**
 * 演示参数化测试的各种方式
 */
@DisplayName("参数化测试示例")
public class ParameterizedTestsDemo {

    /**
     * 使用@ValueSource提供简单参数
     */
    @ParameterizedTest
    @ValueSource(strings = {"racecar", "radar", "able was I ere I saw elba"})
    @DisplayName("测试多个回文字符串")
    void testPalindromes(String candidate) {
        // 测试每个参数是否是回文
        assertTrue(isPalindrome(candidate), 
                 "字符串'" + candidate + "'应该是回文");
    }
    
    /**
     * 使用@ValueSource提供数字参数
     */
    @ParameterizedTest
    @ValueSource(ints = {1, 3, 5, 7, 9})
    @DisplayName("测试多个奇数")
    void testOddNumbers(int number) {
        // 测试每个参数是否是奇数
        assertTrue(number % 2 == 1, number + "应该是奇数");
    }
    
    /**
     * 使用@NullSource, @EmptySource和@NullAndEmptySource
     */
    @ParameterizedTest
    @NullAndEmptySource // 组合了@NullSource和@EmptySource
    @ValueSource(strings = {" ", "   ", "\t", "\n"})
    @DisplayName("测试各种空字符串")
    void testIsBlank(String input) {
        // 测试每个输入是否为空
        assertTrue(isBlank(input), "字符串应该是空的");
    }
    
    /**
     * 使用@EnumSource测试枚举值
     */
    @ParameterizedTest
    @EnumSource(Month.class) // 使用所有Month枚举值
    @DisplayName("测试所有月份")
    void testMonths(Month month) {
        // 验证月份值在有效范围内
        assertNotNull(month);
        assertTrue(month.getValue() >= 1 && month.getValue() <= 12);
    }
    
    /**
     * 使用@EnumSource的subset特性
     */
    @ParameterizedTest
    @EnumSource(
        value = Month.class,
        names = {"APRIL", "JUNE", "SEPTEMBER", "NOVEMBER"},
        mode = EnumSource.Mode.INCLUDE
    )
    @DisplayName("测试30天的月份")
    void testMonthsWith30Days(Month month) {
        // 测试指定的月份是否有30天
        assertEquals(30, month.length(false)); // 非闰年
    }
    
    /**
     * 使用@MethodSource提供复杂参数
     */
    @ParameterizedTest
    @MethodSource("stringIntAndListProvider")
    @DisplayName("从方法获取参数")
    void testWithMultiArgMethodSource(String str, int num, java.util.List<String> list) {
        // 测试提供的多参数
        assertEquals(5, str.length());
        assertTrue(num >= 1 && num <= 10);
        assertEquals(2, list.size());
    }
    
    // 参数提供方法,必须是静态的且返回Stream或可迭代对象
    static Stream<Arguments> stringIntAndListProvider() {
        return Stream.of(
            Arguments.of("apple", 1, java.util.Arrays.asList("a", "b")),
            Arguments.of("lemon", 5, java.util.Arrays.asList("x", "y"))
        );
    }
    
    /**
     * 使用@CsvSource提供CSV格式的参数
     */
    @ParameterizedTest
    @CsvSource({
        "apple,     1",
        "banana,    2",
        "'lemon, lime', 3"  // 带逗号的值需要引号
    })
    @DisplayName("从CSV获取参数")
    void testWithCsvSource(String fruit, int rank) {
        // 测试水果和排名参数
        assertNotNull(fruit);
        assertTrue(rank > 0);
    }
    
    /**
     * 使用@CsvFileSource从CSV文件获取参数
     * 假设test/resources目录下有一个fruits.csv文件
     */
    @ParameterizedTest
    @CsvFileSource(resources = "/fruits.csv", numLinesToSkip = 1)
    @DisplayName("从CSV文件获取参数")
    void testWithCsvFileSource(String name, int count) {
        // 测试从CSV文件读取的参数
        assertNotNull(name);
        assertTrue(count > 0);
    }
    
    /**
     * 自定义显示名称
     */
    @ParameterizedTest(name = "#{index} - 长度({0}) = {1}")
    @CsvSource({
        "hello, 5",
        "world, 5", 
        "JUnit, 5"
    })
    @DisplayName("自定义测试名称")
    void testWithCustomDisplayNames(String input, int expectedLength) {
        // 测试字符串长度
        assertEquals(expectedLength, input.length());
    }
    
    // 判断字符串是否为回文的辅助方法
    private boolean isPalindrome(String str) {
        String clean = str.replaceAll("\\s+", "").toLowerCase();
        StringBuilder reverse = new StringBuilder(clean).reverse();
        return clean.equals(reverse.toString());
    }
    
    // 判断字符串是否为空的辅助方法
    private boolean isBlank(String str) {
        return str == null || str.trim().isEmpty();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153

关键说明:

  • @ParameterizedTest取代了@Test注解,表示这是一个参数化测试
  • 必须结合至少一个参数提供者注解使用,如@ValueSource、@MethodSource等
  • 不同的参数提供者适用于不同场景:
    • @ValueSource:提供简单的单一参数值
    • @NullSource/@EmptySource:提供null或空值
    • @EnumSource:提供枚举类型的值
    • @MethodSource:通过方法提供复杂参数或多个参数
    • @CsvSource:以CSV格式提供参数
    • @CsvFileSource:从CSV文件中读取参数
  • 参数化测试大大减少了重复测试代码,提高了测试效率
  • 可以通过name属性自定义测试名称,使测试报告更易读
编辑此页 (opens new window)
上次更新: 2025/03/21, 11:11:36
Spring Boot - 接口文档
Spring Boot - 内容协商

← Spring Boot - 接口文档 Spring Boot - 内容协商→

Theme by Vdoing | Copyright © 2019-2025 程序员scholar
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式