Java单元测试与集成测试实战:JUnit 5、Mockito与TestContainers深度指南
1. 项目概述:为什么测试是Java开发的“安全带”?
干了这么多年Java开发,我越来越觉得,写代码这事儿,就跟开车上路一样。你车技再好,不系安全带,一次意外就可能让你前功尽弃。而单元测试和集成测试,就是我们程序员写代码时必须系上的“安全带”。这个“Java单元测试与集成测试实战”项目,说白了,就是手把手教你如何给Java项目系上这条可靠的安全带,确保你的代码在上线前是健壮的、可预测的,而不是一个随时可能爆炸的“定时炸弹”。
很多新手,甚至一些工作了几年的朋友,对测试的态度往往是“项目紧,先实现功能,测试以后再说”。结果就是,代码越堆越多,牵一发而动全身,改一个小功能可能引发十个Bug,最后陷入“救火-修复-引入新Bug”的恶性循环,加班加到怀疑人生。单元测试和集成测试,正是为了打破这个循环而生的。它们不是QA(质量保证)团队的专属,而是每个开发者的核心技能和职业素养。通过这个实战指南,你将掌握如何用JUnit、Mockito、TestContainers等主流工具,构建一个从代码块到服务接口的完整测试防护网,让你写的每一行代码都更有底气,让重构和迭代不再是一件提心吊胆的事。
2. 测试体系全景:单元、集成与其他测试的定位与关系
在动手之前,我们必须先理清测试的“家族谱”。测试不是铁板一块,它是一个有层次、有分工的体系。理解每种测试的职责和边界,是高效实践的前提。
2.1 测试金字塔:构建高效测试策略的基石
测试金字塔是一个经典模型,它形象地说明了不同层级测试的理想数量比例。自底向上分别是:
- 单元测试(Unit Tests) :金字塔的塔基,数量最多。它针对最小的可测试单元(通常是单个类或方法)进行隔离测试,速度极快(毫秒级),目的是验证代码逻辑的正确性。
- 集成测试(Integration Tests) :金字塔的中间层,数量适中。它测试多个模块或组件之间的交互是否正确,例如服务与数据库、服务与外部API的集成。速度比单元测试慢,但比UI测试快。
- 端到端测试(E2E Tests) / UI测试 :金字塔的塔尖,数量最少。它模拟真实用户操作,测试整个应用流程。速度最慢,也最脆弱(UI变动易导致失败)。
我们的实战重点在塔基和中间层。一个健康的项目,应该拥有大量快速的单元测试,一定数量的集成测试,以及少量的端到端测试。盲目增加高层测试而忽视底层测试,会导致测试套件运行缓慢、维护成本高昂。
2.2 单元测试 vs. 集成测试:核心区别与选用场景
这是最容易混淆的一对概念。我们可以用一个简单的“用户注册”功能来区分:
-
单元测试(测“点”) :
- 目标 :验证
UserService.register(UserDto dto)方法内部的业务逻辑。例如,密码加密逻辑、用户名重复校验逻辑、数据组装逻辑。 - 方式 : 完全隔离 。使用Mockito等工具,将
UserRepository(数据库访问)、EmailService(邮件服务)等依赖全部“模拟”(Mock)出来。测试只关心UserService自身的代码。 - 断言 :验证方法是否按预期调用了
repository.save()(传入的参数是否正确),或者当用户名重复时是否抛出了特定的业务异常。 - 场景 :所有核心业务逻辑、工具类、算法实现。
- 目标 :验证
-
集成测试(测“线”) :
- 目标 :验证
UserService与真实的UserRepository(连接真实或测试数据库)协作是否正常,或者UserController的HTTP接口能否正确处理请求并返回响应。 - 方式 : 部分集成 。允许与真实的数据库、内存消息队列、或其他内部服务交互,但可能仍会Mock掉像第三方支付网关、短信服务这类不稳定或收费的外部依赖。
- 断言 :调用注册接口后,数据库中是否真的多了一条用户记录;返回的HTTP状态码和JSON格式是否正确。
- 场景 :数据库操作、缓存集成、内部服务间HTTP调用、消息队列的生产与消费。
- 目标 :验证
注意 :单元测试中“单元”的界定有时会有争议。一个类是一个单元,一个方法也是一个单元。我个人的实践是,以一个“类”为单元进行测试组织,但Mock掉其所有外部依赖,确保测试的纯粹性和速度。
2.3 其他相关测试概念:厘清边界
为了避免概念泛化,我们简要提及其他常见测试:
- 功能测试 :通常等同于集成测试或端到端测试,侧重于验证功能是否满足需求,不关心内部实现。
- 系统测试 :在集成测试之后,将整个软件系统作为一个整体进行测试,验证其是否满足规格要求。
- 验收测试 :通常由产品或业务人员主导,验证软件是否满足用户需求和业务目标。
- 性能测试/压力测试 :属于专项测试,评估系统在高负载下的表现,不在本实战主要讨论范围。
理解这些概念后,我们就能清晰地规划: 用单元测试守护每一个逻辑细节,用集成测试验证模块间的协作通路。
3. 单元测试实战:从JUnit 5到Mockito的深度应用
单元测试是我们的第一道,也是最重要的防线。现代Java单元测试几乎离不开JUnit 5和Mockito这对“黄金搭档”。
3.1 环境搭建与JUnit 5核心注解剖析
假设我们使用Maven构建Spring Boot项目。首先在 pom.xml 中引入依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
spring-boot-starter-test 已经包含了JUnit 5、Mockito、AssertJ、Hamcrest等我们所需的大部分库。
JUnit 5的注解是测试类的骨架:
@Test:标记一个方法为测试方法。这是核心。@BeforeEach/@AfterEach:在每个@Test方法 之前 / 之后 运行。常用于初始化测试数据或清理资源。 踩坑提示 :如果@BeforeEach中初始化了被多个测试方法共享的成员变量,务必注意测试的隔离性,避免一个测试修改了数据影响另一个。最佳实践是每个测试方法都自己准备数据。@BeforeAll/@AfterAll:在所有测试方法 之前 / 之后 运行 一次 。方法必须是static。适合做耗时的全局初始化,如启动嵌入式数据库。@DisplayName:为测试类或方法设置一个易读的名称,会在测试报告中显示。强烈建议使用,提高可读性。@Nested:用于创建嵌套的测试类,更好地组织具有共同前提条件的测试。@ParameterizedTest:参数化测试,配合@ValueSource、@CsvSource等使用,可以用多组数据运行同一个测试逻辑,极大减少重复代码。
3.2 测试替身(Mock/Stub/Spy)与Mockito实战
单元测试的核心思想是“隔离”。我们通过创建依赖对象的“替身”来实现。Mockito是最流行的框架。
- Mock :创建一个完全虚拟的对象。你可以设定它的行为(当调用方法A时,返回结果B)。默认情况下,Mock对象的方法返回
null、空集合或0等。 - Spy :创建一个真实对象的“间谍”。默认调用真实方法,但你可以选择性地“存根”(Stub)其中某些方法的行为。
- Stub :打桩,是设定Mock或Spy对象行为的过程。
实战场景 :测试一个 OrderService 的 placeOrder 方法,该方法内部会调用 InventoryService.checkStock() 和 PaymentService.process() 。
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;
@ExtendWith(MockitoExtension.class) // 启用Mockito
public class OrderServiceTest {
@Mock
private InventoryService inventoryService; // 模拟库存服务
@Mock
private PaymentService paymentService; // 模拟支付服务
@InjectMocks
private OrderService orderService; // 将被测服务注入模拟的依赖
@Test
@DisplayName("当库存充足且支付成功时,应成功创建订单")
void placeOrder_Success() {
// 1. 准备测试数据
OrderRequest request = new OrderRequest("item-123", 2);
String transactionId = "txn_001";
// 2. 定义模拟对象的行为(打桩)
when(inventoryService.checkStock("item-123", 2)).thenReturn(true);
when(paymentService.process(any(BigDecimal.class))).thenReturn(transactionId);
// 3. 执行被测方法
OrderResult result = orderService.placeOrder(request);
// 4. 验证结果和行为
assertNotNull(result);
assertTrue(result.isSuccess());
assertEquals(transactionId, result.getTransactionId());
// 验证交互:placeOrder方法是否按预期调用了依赖
verify(inventoryService, times(1)).checkStock("item-123", 2);
verify(paymentService, times(1)).process(any(BigDecimal.class));
// 验证某个方法从未被调用
verify(inventoryService, never()).alertLowStock(anyString());
}
@Test
@DisplayName("当库存不足时,应抛出业务异常且不调用支付")
void placeOrder_Fail_OutOfStock() {
OrderRequest request = new OrderRequest("item-456", 10);
when(inventoryService.checkStock("item-456", 10)).thenReturn(false); // 模拟库存不足
// 断言会抛出特定异常
BusinessException exception = assertThrows(BusinessException.class, () -> {
orderService.placeOrder(request);
});
assertEquals("商品库存不足", exception.getMessage());
// 验证支付服务没有被调用
verify(paymentService, never()).process(any());
}
}
实操心得 :
- Given-When-Then模式 :这是组织测试代码的黄金法则。
Given部分准备数据和Mock行为,When部分执行被测方法,Then部分进行断言和验证。上面的代码注释清晰地体现了这一点,这能让测试逻辑一目了然。 verify的使用:不要过度验证。只验证那些与被测方法业务逻辑直接相关的、重要的交互。过度验证会导致测试脆弱,一旦内部实现细节(如调用顺序、无关紧要的日志方法)改变,测试就会失败,尽管功能可能依然正确。any()等参数匹配器:当不关心具体参数值时使用。但要小心,如果方法重载,any()可能匹配不到你期望的方法。
3.3 断言库的选择:JUnit 5 vs. AssertJ
JUnit 5自带的 Assertions 类已经够用,但AssertJ提供了流式API,断言更强大、可读性更高。
// JUnit 5 断言
assertEquals(expected, actual);
assertTrue(collection.isEmpty());
assertThrows(SomeException.class, () -> service.doSomething());
// AssertJ 断言 (更推荐)
import static org.assertj.core.api.Assertions.*;
assertThat(actual).isEqualTo(expected);
assertThat(collection).isEmpty();
assertThatThrownBy(() -> service.doSomething())
.isInstanceOf(SomeException.class)
.hasMessageContaining("expected error");
// 链式调用,支持集合、对象属性、字符串等多种断言
assertThat(userList)
.hasSize(3)
.extracting(User::getName)
.containsExactly("Alice", "Bob", "Charlie");
建议 :在新项目中直接使用AssertJ,它的错误信息更友好,能极大提升调试效率。
4. 集成测试实战:连接真实组件的测试艺术
集成测试验证的是协作。在Spring生态中,我们主要测试:1) 数据层与数据库;2) Web层与控制器的HTTP接口。
4.1 数据层集成测试:@DataJpaTest与Testcontainers
对于Spring Data JPA的Repository测试,Spring Boot提供了 @DataJpaTest 注解。它会自动配置一个内存数据库(如H2),只初始化JPA相关的组件。
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.beans.factory.annotation.Autowired;
@DataJpaTest // 关键注解
public class UserRepositoryTest {
@Autowired
private UserRepository userRepository;
@Test
@DisplayName("根据邮箱查找用户 - 集成测试")
void findByEmail_ShouldReturnUser() {
// Given: 直接操作Repository保存数据到(内存)数据库
User savedUser = new User("test@example.com", "encodedPassword");
userRepository.save(savedUser);
// When: 执行查询方法
Optional<User> found = userRepository.findByEmail("test@example.com");
// Then: 验证数据库交互结果
assertThat(found).isPresent();
assertThat(found.get().getEmail()).isEqualTo("test@example.com");
}
}
但这里有个大坑 : @DataJpaTest 默认使用H2内存数据库。H2与MySQL、PostgreSQL等生产数据库存在语法和功能差异(如窗口函数、特定JSON函数、约束行为),可能导致测试通过,上线失败。
解决方案:使用Testcontainers 。它能在Docker容器中启动一个真实的生产数据库进行测试。
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
@Testcontainers // 启用Testcontainers支持
@DataJpaTest
// 关键:覆盖数据源配置,指向Testcontainer启动的数据库
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
public class UserRepositoryWithRealDbTest {
// 定义一个共享的PostgreSQL容器
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15-alpine")
.withDatabaseName("testdb")
.withUsername("test")
.withPassword("test");
// 在Spring上下文初始化前,动态设置数据库连接属性
@DynamicPropertySource
static void configureProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", postgres::getJdbcUrl);
registry.add("spring.datasource.username", postgres::getUsername);
registry.add("spring.datasource.password", postgres::getPassword);
}
@Autowired
private UserRepository userRepository;
@Test
void testWithRealPostgres() {
// 测试逻辑与之前类似,但这次运行在真实的PostgreSQL中
User user = new User("real@test.com", "pwd");
userRepository.save(user);
assertThat(userRepository.findByEmail("real@test.com")).isPresent();
}
}
实操心得 :虽然Testcontainers测试比内存数据库慢(需要拉取镜像、启动容器),但它提供了无与伦比的真实性。建议在CI/CD流水线中运行这类集成测试,本地开发时可根据需要选择性地运行。对于复杂的SQL或数据库特性相关的逻辑,必须使用Testcontainers。
4.2 Web层集成测试:@WebMvcTest与MockMvc
对于Controller的测试,我们使用 @WebMvcTest 。它会切片加载Web层相关的配置,默认会Mock掉Service层的Bean,让我们可以专注于HTTP请求和响应的测试。
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import com.fasterxml.jackson.databind.ObjectMapper;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@WebMvcTest(UserController.class) // 只加载UserController相关的Web配置
public class UserControllerTest {
@Autowired
private MockMvc mockMvc; // 模拟HTTP请求的核心工具
@Autowired
private ObjectMapper objectMapper; // JSON序列化/反序列化
@MockBean // 模拟Service,因为@WebMvcTest不会加载@Service
private UserService userService;
@Test
@DisplayName("POST /api/users - 创建用户成功")
void createUser_ShouldReturnCreated() throws Exception {
UserDto requestDto = new UserDto("newuser@example.com", "password123");
UserResponse response = new UserResponse(1L, "newuser@example.com");
when(userService.createUser(any(UserDto.class))).thenReturn(response);
mockMvc.perform(post("/api/users") // 模拟POST请求
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(requestDto)))
.andExpect(status().isCreated()) // 断言HTTP状态码为201
.andExpect(jsonPath("$.id").value(1L)) // 断言JSON响应体
.andExpect(jsonPath("$.email").value("newuser@example.com"));
}
@Test
@DisplayName("GET /api/users/{id} - 用户不存在时返回404")
void getUserById_ShouldReturn404_WhenUserNotFound() throws Exception {
when(userService.getUserById(999L)).thenThrow(new UserNotFoundException("用户不存在"));
mockMvc.perform(get("/api/users/{id}", 999L))
.andExpect(status().isNotFound())
.andExpect(jsonPath("$.message").value("用户不存在"));
}
}
注意事项 :
@MockBean是Spring提供的注解,用于在Spring的ApplicationContext中Mock一个Bean。它与Mockito的@Mock不同,后者需要在@ExtendWith(MockitoExtension.class)下使用,且不涉及Spring容器。MockMvc提供了非常强大的请求构建和结果验证能力,可以测试请求头、参数、会话、异常处理等几乎所有Web层特性。- 对于RESTful API,务必测试边界情况和错误场景(如无效输入、资源不存在、权限不足等),这比只测试“happy path”更重要。
4.3 全应用集成测试:@SpringBootTest
当需要测试多个层级(如从Controller到Repository)的完整流程时,可以使用 @SpringBootTest 。它会启动一个完整的、但可能是轻量级的应用上下文。
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.HttpStatus;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
// 使用随机端口,避免冲突
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Testcontainers
public class UserIntegrationTest {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15-alpine");
@DynamicPropertySource
static void configureProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", postgres::getJdbcUrl);
// ... 同上
}
@Autowired
private TestRestTemplate restTemplate; // 用于发起真实HTTP请求
@Test
void fullIntegration_CreateAndRetrieveUser() {
// 1. 创建用户
UserDto request = new UserDto("int-test@example.com", "pass");
ResponseEntity<UserResponse> createResponse = restTemplate.postForEntity(
"/api/users", request, UserResponse.class);
assertThat(createResponse.getStatusCode()).isEqualTo(HttpStatus.CREATED);
Long userId = createResponse.getBody().getId();
// 2. 查询用户
ResponseEntity<UserResponse> getResponse = restTemplate.getForEntity(
"/api/users/" + userId, UserResponse.class);
assertThat(getResponse.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(getResponse.getBody().getEmail()).isEqualTo("int-test@example.com");
// 3. 验证数据库(可通过注入Repository进行额外断言)
// ...
}
}
使用建议 : @SpringBootTest 启动较慢,应谨慎使用。通常用于测试涉及复杂工作流、事务管理、安全配置等需要完整上下文的场景。在CI流水线中,可以将其作为“集成测试套件”的一部分运行。
5. 测试代码的质量与可维护性
写出能运行的测试只是第一步,写出 好 的测试才能长期受益。糟糕的测试比没有测试更可怕,因为它会带来虚假的安全感和高昂的维护成本。
5.1 好测试的FIRST原则
- F - Fast(快速) :测试必须快。单元测试应在毫秒级,集成测试也尽量控制在秒级。慢的测试会导致开发者不愿意运行。
- I - Independent/Isolated(独立/隔离) :测试之间不应有依赖,可以以任何顺序运行。避免使用共享的静态变量或未清理的数据库状态。
- R - Repeatable(可重复) :在任何环境(本地、CI服务器)中运行都能得到相同的结果。不能依赖网络、时间、随机数等外部不确定因素。
- S - Self-Validating(自验证) :测试必须能自动判断通过还是失败,无需人工检查日志或输出。
- T - Timely(及时) :最好在编写生产代码的同时或之前编写测试(TDD)。
5.2 测试命名与结构的最佳实践
- 命名 :使用
方法名_测试场景_预期结果的格式。例如:placeOrder_WhenStockInsufficient_ThrowsException。@DisplayName可以用更自然的中文或英文句子描述。 - 结构 :严格遵守 Arrange-Act-Assert (AAA) 或 Given-When-Then 模式。用空行将三个部分隔开,让代码结构清晰。
- 单一职责 :一个测试方法只测试一个行为或场景。不要在一个方法里验证多种条件。
- DRY(Don‘t Repeat Yourself)原则 :将通用的准备代码(如创建测试对象)提取到
@BeforeEach方法或工具类中。但要注意,过度提取可能会降低测试的可读性,因为读者需要在多个地方跳转才能理解测试上下文。
5.3 测试数据管理:@Sql与测试数据工厂
对于集成测试,准备和清理测试数据是关键。
-
@Sql注解 :Spring Test提供的注解,用于在测试方法前后执行指定的SQL脚本。@Test @Sql(scripts = "/insert-test-users.sql") // 测试前执行 @Sql(scripts = "/cleanup-users.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) // 测试后执行 void testWithSqlScript() { // 测试逻辑... } - 测试数据工厂(Test Data Factory) :创建一个专门生成测试对象的工厂类。这比在多个测试中重复构建对象更好,也便于统一修改。
public class TestUserFactory { public static User createValidUser() { return User.builder() .email("user@test.com") .passwordHash("encodedHash") .status(Status.ACTIVE) .build(); } public static UserDto createValidUserDto() { ... } // 可以创建各种边界情况的工厂方法 public static User createUserWithLongEmail() { ... } }
5.4 测试覆盖率:目标与陷阱
Jacoco是常用的Java代码覆盖率工具。覆盖率是一个重要的度量指标,但切勿盲目追求高数字。
- 行覆盖率 :至少应达到70%-80%,这是基本要求。
- 分支覆盖率 :更重要,它衡量条件语句(if/else, switch)是否都被测试到。
- 陷阱 :
- 覆盖率高 != 质量高 :测试可能覆盖了所有行,但断言(Assertion)很弱,没有验证核心逻辑。
- 不要为了覆盖率而写测试 :有些代码(如简单的Getter/Setter、自动生成的Mapper方法)不值得测试。有些复杂的私有方法,应该通过测试其公有调用来间接覆盖。
- 覆盖率是发现未测代码的工具,不是终极目标 :它的主要价值是帮你发现哪些代码完全没有被测试到,而不是炫耀一个百分比。
6. 持续集成中的测试策略与常见问题排查
测试最终要融入到开发流程中,持续集成(CI)是保证测试持续运行的关键环节。
6.1 在CI流水线中组织测试阶段
一个典型的CI流水线(如Jenkins、GitLab CI、GitHub Actions)应包含以下测试阶段:
- 代码质量检查 :先运行SpotBugs、Checkstyle、PMD等静态代码分析。
- 快速单元测试 :运行所有单元测试。这阶段必须快,失败应立即反馈。
- 集成测试 :运行集成测试套件。这阶段可以稍慢,但应并行化以缩短总时间。使用Testcontainers时,确保CI环境安装了Docker。
- 端到端测试 (可选):如果项目有,可以在此阶段运行。
- 构建与部署 :所有测试通过后,才进行打包和部署到测试环境。
配置示例(GitHub Actions) :
name: Java CI
on: [push]
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:15-alpine
env: ...
steps:
- uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v3
with: { java-version: '17' }
- name: Run Unit Tests
run: mvn test
- name: Run Integration Tests
run: mvn verify -DskipUnitTests=false -DskipITs=false
# 通常通过Maven的failsafe插件来区分单元测试和集成测试
6.2 常见问题与排查技巧实录
即使经验丰富,写测试也会遇到各种坑。下面是一些高频问题及解决思路:
| 问题现象 | 可能原因 | 排查与解决思路 |
|---|---|---|
@MockBean 注入的Bean为null |
测试类使用了错误的测试切片注解或Runner。 | 确保使用 @WebMvcTest 、 @DataJpaTest 等切片注解,或使用 @SpringBootTest 。检查是否错误地混用了 @ExtendWith(MockitoExtension.class) 和 @SpringBootTest 。 |
| 集成测试时数据库连接失败 | 配置错误,或Testcontainers容器未启动。 | 1. 检查 application-test.properties 配置。2. 确保CI环境支持Docker。3. 在 @Testcontainers 类中,容器字段需为 static 。4. 查看日志,确认容器JDBC URL是否正确注入。 |
| 测试在CI上通过,本地失败(或反之) | 环境差异:时区、文件路径、端口占用、数据库数据残留。 | 1. 使用一致的数据库 :强烈推荐Testcontainers。2. 清理测试数据 :确保每个测试独立,使用 @Transactional (小心回滚行为)或手动清理。3. 固定随机种子 :如果测试涉及随机数。 |
| Mockito: “UnnecessaryStubbingException” | 测试中定义了Mock行为,但实际执行路径并未用到。 | 这是Mockito的严格检查,有助于保持测试简洁。使用 @MockitoSettings(strictness = Strictness.LENIENT) 放宽检查,或更好的方法是 重构测试 ,移除无用的 when() 语句。 |
| 测试运行速度突然变慢 | 1. 构建了完整的Spring上下文( @SpringBootTest )。 2. 测试未正确隔离,启动了不必要的服务或容器。 3. 单个测试方法做了太多事。 |
1. 优先使用切片测试( @WebMvcTest , @DataJpaTest )。 2. 审视集成测试范围,是否可以用单元测试替代部分场景。 3. 利用 @BeforeAll 代替重复的 @BeforeEach 初始化(如果安全)。 4. 并行运行测试(JUnit 5支持)。 |
涉及时间(如 new Date() )的测试不稳定 |
测试依赖于当前时间,不同时间运行结果可能不同。 | 使用“时间旅行”工具 :如 java.time.Clock 或第三方库(如 org.mockito:mockito-inline 可以Mock静态方法)。将时间源作为依赖注入,在测试中固定它。 |
| 事务回滚相关问题 | 在集成测试中使用了 @Transactional ,导致测试后数据自动回滚,但某些验证(如异步操作、独立数据库连接)需要看到提交的数据。 |
理解 @Transactional 在测试中的默认行为(默认回滚)。对于需要提交的测试,可以使用 @Commit 注解,或者手动管理事务,并在测试后清理数据。 |
我个人最深刻的体会是 :测试不是负担,而是投资。初期投入时间编写高质量的测试,会在代码重构、功能扩展、新人接手、定位线上问题时,十倍百倍地回报你。它让你有勇气去修改任何一段代码,因为你知道有测试网在兜底。开始可能觉得繁琐,但一旦形成习惯,它就会成为你开发流程中不可或缺的、令人安心的一部分。从今天起,尝试为你修复的下一个Bug或写的新功能,先补上一个测试吧,你会感受到它带来的变化。
更多推荐

所有评论(0)