SpringBoot测试最佳实践:JUnit5与Mockito构建高效测试体系
1. 项目概述:为什么我们需要“最佳实践”?
在SpringBoot项目里写单元测试和集成测试,这事儿听起来简单,但真动起手来,十个开发者里得有八个踩过坑。我见过太多项目,测试代码写得比业务代码还难懂,或者干脆就是一堆“伪测试”——跑是能跑通,但业务逻辑一改,测试要么全挂,要么毫无感知。问题出在哪?往往不是JUnit或者Mockito这些工具本身,而是没有一套清晰、可落地、能贯穿项目生命周期的“最佳实践”。
这个标题里的关键词很明确: SpringBoot 、 单元测试 、 Mockito 、 JUnit5 、 集成测试 。把它们串起来,核心要解决的就是一个现代Java后端项目中的测试体系问题。单元测试负责验证单个“零件”(如一个Service方法)的逻辑正确性,要求快速、隔离;集成测试则关注“零件”组装后的协作,比如Controller调用Service、Service访问数据库这一整条链路是否通畅。Mockito在这里扮演“替身演员”的角色,把那些复杂的、慢速的依赖(如数据库、第三方API)模拟出来,让我们能专注于测试目标代码本身。而JUnit5是这一切的舞台和规则制定者。
所以,这篇实战总结,就是想把我这些年趟过的坑、总结出来的套路,系统地梳理一遍。目标不是教你某个注解怎么用(那是官方文档的事),而是告诉你,在一个真实的、持续迭代的SpringBoot项目里,如何搭建一个 健壮、可维护、高效 的测试套件。你会看到如何选择测试策略、如何优雅地使用Mockito、如何利用JUnit5的新特性,以及如何让集成测试既可靠又不至于慢得让人无法忍受。
2. 测试策略与框架选型:不只是版本号那么简单
在动手写第一行测试代码之前,先想清楚测试策略,这能省下后期大量的重构时间。很多人一上来就 @SpringBootTest 拉满整个应用上下文,然后抱怨测试跑得太慢。这其实是策略上的失误。
2.1 单元测试 vs. 集成测试:明确边界
首先必须厘清概念,因为两者的编写方式和目标截然不同。
- 单元测试 :测试对象是 单个类 (通常是Service、Util或Mapper)中的 单个方法 。核心原则是 隔离 。所有外部依赖(如其他的Service、Repository、第三方客户端)都必须被Mock或Stub。它的执行速度应该极快(毫秒级),并且不依赖Spring容器、数据库、网络等外部环境。在SpringBoot项目中,这意味着你通常 不应该 使用
@SpringBootTest来写单元测试,而是用@ExtendWith(MockitoExtension.class)这类轻量级扩展。 - 集成测试 :测试对象是 多个组件 之间的交互。例如,测试一个REST API从Controller到Service再到Repository的完整调用链路,或者测试某个配置类是否正确生效。它需要启动Spring容器,可能会连接真实的数据库或内存数据库(如H2)。它的执行速度较慢,但能发现组件集成时才会出现的问题。
一个常见的误区是把所有测试都写成集成测试。正确的做法是建立 测试金字塔 :底层是大量快速的单元测试,中间是适量的集成测试,顶层是极少量的端到端(E2E)测试。我们的精力应该主要投入到单元测试上。
2.2 JUnit5:为什么是它,而不再是JUnit4?
JUnit5不仅仅是一个版本升级,它是一次架构重构。核心优势在于其模块化(JUnit Platform + Jupiter + Vintage)和强大的扩展模型。
- 更灵活的生命周期注解 :
@BeforeEach、@AfterEach替代了@Before、@After,语义更清晰。@BeforeAll、@AfterAll必须是静态方法的规定,也提醒我们这些方法用于昂贵的初始化。 - 强大的断言库 :
AssertJ或 JUnit5自带的Assertions提供了流式API,断言失败时的信息更友好。例如,assertThat(actualList).containsExactlyInAnyOrder("a", "b", "c")。 - 动态测试与参数化测试 :
@TestFactory允许运行时生成测试用例,@ParameterizedTest配合@ValueSource、@CsvSource等,让数据驱动测试变得异常简单,极大地减少了重复代码。 - 标签与条件执行 :
@Tag可以对测试分类(如”slow”, “integration”),方便用Maven/Gradle命令选择性执行。@EnabledOnOs、@DisabledIf等条件注解,让测试能智能地适应不同环境。
选型结论 :对于任何新启动的SpringBoot项目(通常使用Spring Boot 2.2+),应毫不犹豫地选择JUnit5作为测试框架的基石。Spring Boot Starter Test默认就包含了JUnit5。
2.3 Mockito:模拟的艺术与陷阱
Mockito是Java领域事实上的Mock框架标准。它的核心是创建“模拟对象”来替代真实依赖。但“会用”和“用好”之间有巨大差距。
-
@Mockvs@Spy:这是最容易混淆的点。@Mock创建的是一个“完全虚假”的对象。所有方法默认返回空值(0, false, null, 空集合)。除非你用when(...).thenReturn(...)明确指定行为,否则调用它的任何方法都不会执行真实逻辑。@Spy创建的是一个“部分真实”的对象,它包装了一个真实实例。除非你明确地Stub了某个方法,否则调用它会执行真实对象的逻辑。 慎用Spy ,因为它让你的测试与实现细节耦合更紧,通常意味着你的类可能职责过重,需要拆分。
-
@InjectMocks:这个注解会自动将当前测试类中由@Mock(或@Spy)创建的模拟对象,注入到被测试类的实例中。它是快速完成依赖注入的利器。但注意,它用的是 反射 ,对于复杂的继承链或多个构造器,可能会不如显式调用构造器清晰。 - 行为验证(Verify) :Mockito允许你验证模拟对象是否以预期的参数被调用了预期的次数。例如,
verify(userRepository, times(1)).findById(userId)。这是单元测试中验证“协作”行为的关键。但要避免过度验证(verifying every single call),这会让测试变得脆弱。
实操心得 :我个人的原则是, 优先使用
@Mock。只有当你想验证某个对象的 部分 方法,而其他方法仍需真实逻辑时(这种情况很少),才考虑@Spy。对于@InjectMocks,在简单的Service测试中很好用,但如果依赖较多或构造复杂,我更喜欢在@BeforeEach方法里用构造器手动创建被测试对象,这样依赖关系一目了然。
3. 单元测试实战:构建坚固的“零件”质检线
单元测试是我们的主战场。目标是写出 专注、快速、稳定 的测试。
3.1 测试环境搭建:轻装上阵
对于纯单元测试,我们不需要启动整个Spring Boot应用。在Maven项目中,确保 pom.xml 引入了正确的依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<!-- Spring Boot 2.7+ 默认排除了JUnit4,使用JUnit5 -->
<exclusions>
<exclusion>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</exclusion>
</exclusions>
</dependency>
spring-boot-starter-test 已经包含了JUnit5, Mockito, AssertJ, Hamcrest等我们所需的一切。
创建一个Service的单元测试类:
import org.junit.jupiter.api.Test; // JUnit5
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.assertj.core.api.Assertions.*;
@ExtendWith(MockitoExtension.class) // 关键!使用Mockito扩展,替代@RunWith
class UserServiceUnitTest {
@Mock
private UserRepository userRepository; // 模拟依赖
@InjectMocks
private UserService userService; // 被测试类,自动注入@Mock对象
@Test
void getUserById_shouldReturnUser_whenUserExists() {
// 1. 准备数据 (Arrange)
Long userId = 1L;
User expectedUser = new User(userId, "Alice");
when(userRepository.findById(userId)).thenReturn(Optional.of(expectedUser));
// 2. 执行操作 (Act)
User actualUser = userService.getUserById(userId);
// 3. 验证结果 (Assert)
assertThat(actualUser).isNotNull();
assertThat(actualUser.getId()).isEqualTo(userId);
assertThat(actualUser.getName()).isEqualTo("Alice");
// 验证交互行为
verify(userRepository, times(1)).findById(userId);
verify(userRepository, never()).deleteById(any()); // 确保未调用其他方法
}
@Test
void getUserById_shouldThrowException_whenUserNotFound() {
// Arrange
Long userId = 999L;
when(userRepository.findById(userId)).thenReturn(Optional.empty());
// Act & Assert
assertThatThrownBy(() -> userService.getUserById(userId))
.isInstanceOf(UserNotFoundException.class)
.hasMessageContaining("User not found");
verify(userRepository).findById(userId);
}
}
代码解读 :
@ExtendWith(MockitoExtension.class):这是JUnit5的方式,用于启用Mockito支持。它替代了JUnit4的@RunWith(MockitoJUnitRunner.class)。- Given-When-Then模式 :这是组织测试代码的经典模式(对应注释中的Arrange-Act-Assert),能让测试逻辑非常清晰。
when(...).thenReturn(...):这是Stubbing(桩定),定义当模拟方法被调用时返回什么。assertThat(...):来自AssertJ,流式断言,可读性极强。verify(...):行为验证,确保依赖被以正确的方式调用。
3.2 处理复杂依赖与异常流
现实中的业务逻辑不会总是 findById 这么简单。比如一个订单服务,创建订单时需要检查库存、计算价格、保存订单、发送消息。
@Test
void createOrder_shouldSuccess_whenInventorySufficient() {
// Arrange
OrderRequest request = new OrderRequest(/* ... */);
Product mockProduct = new Product(/* ... */);
when(inventoryService.checkStock(any())).thenReturn(true);
when(priceCalculator.calculate(any())).thenReturn(new BigDecimal("100.00"));
when(orderRepository.save(any(Order.class))).thenAnswer(invocation -> {
Order orderToSave = invocation.getArgument(0);
orderToSave.setId(1000L); // 模拟保存后生成ID
return orderToSave;
});
doNothing().when(messageQueueService).sendOrderCreatedEvent(anyLong());
// Act
Order result = orderService.createOrder(request);
// Assert
assertThat(result.getId()).isEqualTo(1000L);
assertThat(result.getStatus()).isEqualTo(OrderStatus.CREATED);
verify(inventoryService).checkStock(any());
verify(orderRepository).save(any(Order.class));
verify(messageQueueService).sendOrderCreatedEvent(1000L);
}
@Test
void createOrder_shouldFail_whenInventoryInsufficient() {
// Arrange
when(inventoryService.checkStock(any())).thenReturn(false);
// Act & Assert
assertThatThrownBy(() -> orderService.createOrder(new OrderRequest()))
.isInstanceOf(InsufficientInventoryException.class);
// 验证库存检查后,后续的保存和发送消息操作一定不能发生
verify(orderRepository, never()).save(any());
verify(messageQueueService, never()).sendOrderCreatedEvent(anyLong());
}
关键技巧 :
thenAnswer:当需要根据输入参数动态决定返回值或执行一些操作时使用,比thenReturn更灵活。doNothing().when(...):用于Stub返回类型为void的方法。- 验证调用顺序 :在复杂流程中,有时需要验证方法调用顺序。可以使用
InOrder:InOrder inOrder = inOrder(repo1, repo2); inOrder.verify(repo1).callFirst(); inOrder.verify(repo2).callSecond();。 -
never()验证 :在异常流测试中,确保某些方法一定没有被调用,这和确保某些方法被调用同样重要。
3.3 参数化测试:用数据驱动覆盖边界
JUnit5的参数化测试能大幅减少重复的测试方法。
@ParameterizedTest
@CsvSource({
"0, 0, 0",
"1, 2, 3",
"-1, 1, 0",
"100, 200, 300"
})
@DisplayName("加法运算测试: {0} + {1} = {2}")
void testAdd(int a, int b, int expectedSum) {
Calculator calculator = new Calculator();
int result = calculator.add(a, b);
assertThat(result).isEqualTo(expectedSum);
}
// 测试边界情况:用户状态转换
@ParameterizedTest
@EnumSource(value = UserStatus.class, names = {"ACTIVE", "SUSPENDED"})
void shouldAllowLogin_whenStatusIsNotDisabled(UserStatus status) {
User user = new User();
user.setStatus(status);
assertThat(loginService.canLogin(user)).isTrue();
}
@ParameterizedTest
@ValueSource(strings = {"", " ", "\t", "\n"})
void shouldThrowException_whenUsernameIsBlank(String blankUsername) {
assertThatThrownBy(() -> userService.createUser(blankUsername, "password"))
.isInstanceOf(InvalidArgumentException.class);
}
注意事项 :
@DisplayName可以生成更易读的测试报告。@EnumSource用于遍历枚举。@ValueSource用于提供一组基本类型的值。@CsvSource用于提供多参数组合,非常适合测试多种输入输出组合的场景。
4. 集成测试实战:验证“组装”后的运行状态
集成测试需要启动Spring容器,因此我们会用到 @SpringBootTest 。关键是如何让它既有效又不笨重。
4.1 @SpringBootTest 的精准控制
默认情况下, @SpringBootTest 会加载完整的应用上下文,这很慢。我们可以通过注解参数进行优化。
// 示例1:测试Web层(Controller)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) // 启动真实Tomcat在随机端口
@AutoConfigureMockMvc // 即使使用RANDOM_PORT,也可以注入MockMvc进行切片测试(更轻量),但这里我们用TestRestTemplate
class UserControllerIntegrationTest {
@Autowired
private TestRestTemplate restTemplate; // 用于发起HTTP请求
@Test
void getUserApi_shouldReturnUser() {
ResponseEntity<User> response = restTemplate.getForEntity("/api/users/1", User.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(response.getBody()).isNotNull();
assertThat(response.getBody().getName()).isEqualTo("Test User");
}
}
// 示例2:测试数据层(Repository)与数据库交互
@DataJpaTest // 关键!只加载JPA相关的配置,使用嵌入式数据库(如H2)
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.ANY) // 使用内存数据库替代
class UserRepositoryIntegrationTest {
@Autowired
private TestEntityManager entityManager; // 用于持久化测试数据
@Autowired
private UserRepository userRepository;
@Test
void shouldFindUserByName() {
// 先准备数据到“测试数据库”
User user = new User("John Doe");
entityManager.persist(user);
entityManager.flush();
// 执行查询
Optional<User> found = userRepository.findByName("John Doe");
// 验证
assertThat(found).isPresent();
assertThat(found.get().getName()).isEqualTo("John Doe");
}
}
核心优化点 :
-
@DataJpaTest:只初始化数据层相关的Bean(Repository, EntityManager, DataSource),速度极快。它默认会回滚事务,保证测试隔离。 -
@WebMvcTest:只初始化Web层相关的Bean(Controller, ControllerAdvice, Filter),不加载Service和Repository。适合单独测试Controller逻辑,需要Mock Service层。 -
@JsonTest:只初始化JSON序列化相关的组件,用于测试Jackson的序列化/反序列化。 -
@AutoConfigureTestDatabase:强制使用内存数据库,避免污染和依赖外部真实数据库。 -
webEnvironment:WebEnvironment.MOCK:默认值,不启动真实Servlet容器,使用Mock环境。WebEnvironment.RANDOM_PORT:启动真实容器在随机端口,适合需要测试完整HTTP栈的集成测试。WebEnvironment.NONE:不提供任何Web环境,用于非Web测试。
4.2 测试切片(Test Slices)与MockBean
Spring Boot的“测试切片”思想是集成测试高效的关键。它允许你只加载你关心的那部分应用上下文。
// 使用 @WebMvcTest 切片测试Controller,并Mock Service层
@WebMvcTest(UserController.class) // 只加载UserController及其依赖的Web组件
class UserControllerSliceTest {
@Autowired
private MockMvc mockMvc; // 模拟HTTP请求的利器
@MockBean // 关键!在ApplicationContext中注入一个Mockito Mock,替代真实的Bean
private UserService userService;
@Test
void getUser_shouldReturnOk() throws Exception {
User mockUser = new User(1L, "Mocked Alice");
when(userService.getUserById(1L)).thenReturn(mockUser);
mockMvc.perform(get("/api/users/1")
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$.name").value("Mocked Alice"));
verify(userService).getUserById(1L);
}
}
@MockBean vs @Mock :
@Mock是Mockito的注解,在纯单元测试中由MockitoExtension管理。@MockBean是Spring Boot的注解,用于在集成测试的Spring容器中,将一个Bean替换为Mockito Mock。当你的测试切片(如@WebMvcTest)需要某个依赖,但你又不希望启动它的真实实现时,就用@MockBean。
4.3 测试事务与数据回滚
集成测试经常涉及数据库操作。为了保证测试之间互不干扰,必须处理好数据清理。
@DataJpaTest
@Transactional // 默认就是开启的,每个测试方法都在事务中,方法结束后自动回滚
class TransactionalTest {
@Autowired
private UserRepository repository;
@Test
void testWithRollback() {
User user = new User("Test");
repository.save(user);
// 此时在同一个事务内,可以查询到
assertThat(repository.count()).isEqualTo(1);
}
// 测试方法结束后,所有数据库操作回滚,不影响下一个测试
}
// 如果你确实需要提交数据(例如测试事务传播行为),可以禁用事务
@Test
@Transactional(propagation = Propagation.NOT_SUPPORTED) // 不在事务中运行
void testWithoutTransaction() {
// 这里的数据操作会提交到数据库,需要手动清理
}
实操心得 : 99%的集成测试都应该在事务中运行并自动回滚 。这是保证测试独立性的黄金法则。对于极少数需要测试事务提交后状态的场景,务必在
@AfterEach或@AfterAll方法中编写明确的数据清理逻辑(如repository.deleteAll()),并考虑使用独立的测试数据库或Schema。
5. 高级技巧与最佳实践提炼
掌握了基础写法后,一些高级技巧和团队约定能让测试代码的质量再上一个台阶。
5.1 测试代码的可读性与维护性
糟糕的测试代码是项目的债务。遵循以下原则:
- 命名即文档 :测试方法名应该清晰地表达场景和预期。使用
should_[行为]_when_[条件]或[方法名]_[场景]_[预期]的格式。例如:shouldReturnEmptyList_whenDatabaseIsEmpty。 - 单一职责 :一个测试方法只测试一个逻辑分支或场景。不要在一个方法里测完正常流又测异常流。
- Given-When-Then结构 :严格遵循三段式,用空行分隔,让代码块意图分明。
- 提取通用代码 :将通用的数据准备(如构建一个复杂的对象)提取到
@BeforeEach方法或静态工厂方法中。但要注意,@BeforeEach中的代码应该是所有测试 真正需要 的,不要为了复用而复用。 - 使用自定义断言(Custom Assertions) :对于复杂对象的断言,可以封装成自定义的AssertJ断言,提高可读性。
// 自定义断言类 public class UserAssert extends AbstractAssert<UserAssert, User> { public UserAssert hasName(String expectedName) { isNotNull(); if (!actual.getName().equals(expectedName)) { failWithMessage("Expected user's name to be <%s> but was <%s>", expectedName, actual.getName()); } return this; } // 使用 assertThat(actualUser).hasName("Alice").hasStatus(Status.ACTIVE); }
5.2 测试替身(Test Double)的深度使用
Mockito提供了丰富的Stubbing和验证功能。
- 参数匹配器(Argument Matchers) :
any(),eq(),isNull(),argThat()等。使用any()时要小心,它匹配任何值,包括null。如果方法参数是基本类型,要用对应的anyInt(),anyString()等。 - 验证调用次数和超时 :
times(n),atLeast(n),atMost(n),never()。verify(mock, timeout(100)).someMethod()用于验证异步调用。 - 按顺序验证(Verification in Order) :如前所述,使用
InOrder对象。 - 重置Mock(Reset Mocks) : 一般不推荐在测试中使用
reset(mock)。这通常是测试方法间存在不必要的耦合或测试设计有问题的信号。每个测试方法应该独立地设置其所需的Mock行为。
5.3 常见陷阱与排查技巧
即使经验丰富,也难免踩坑。下面是一些高频问题及解决方案:
| 问题现象 | 可能原因 | 排查与解决 |
|---|---|---|
@MockBean 注入的Mock行为不生效 |
1. 可能被其他配置类中定义的真实Bean覆盖了。 2. 测试类上可能有多个 @SpringBootTest 相关的注解冲突。 |
1. 检查测试配置,确保没有 @Primary 的真实Bean。 2. 使用 @TestPropertySource 或 @SpringBootTest 的 properties 属性来调整Bean加载顺序。简化测试配置,优先使用测试切片。 |
| 集成测试启动奇慢无比 | 1. 使用了 @SpringBootTest 且未指定 webEnvironment 或 classes ,加载了全量上下文。 2. 应用本身依赖多、初始化复杂。 3. 每次测试都重新构建上下文。 |
1. 使用测试切片 ( @WebMvcTest , @DataJpaTest )。 2. 如果必须用全上下文,考虑使用 @SpringBootTest(classes = {YourSpecificConfig.class}) 缩小范围。 3. 利用 @DirtiesContext 控制上下文缓存,但慎用,因为它会破坏测试速度。 |
| 事务回滚失效,测试数据污染 | 1. 测试方法或类上标记了 @Transactional(propagation = Propagation.NOT_SUPPORTED) 。 2. 在测试方法中手动调用了 entityManager.flush() 且未在事务内。 3. 使用了非事务性的存储(如某些NoSQL客户端)。 |
1. 确保测试在默认的 @Transactional 环境下运行。 2. 检查数据库表引擎是否支持事务(如MySQL的MyISAM不支持)。 3. 对于无法回滚的操作,在 @AfterEach 中编写清理脚本。 |
| Mockito “Argument(s) are different” 错误 | 在 verify 或 when 时,实际传入的参数与预期( eq(value) )不匹配。可能是对象引用不同或字段值不同。 |
1. 使用 any() 等匹配器放宽验证。 2. 使用 ArgumentCaptor 捕获实际参数进行详细断言。 3. 确保测试数据对象正确实现了 equals() 和 hashCode() 方法,以便 eq() 能正确工作。 |
| 测试在CI环境失败,本地却成功 | 1. 环境差异(数据库、时区、文件路径)。 2. 测试依赖未清理的共享状态(如静态变量)。 3. 并发执行测试导致冲突。 |
1. 使用内存数据库(H2)并模拟生产环境模式 (如 spring.jpa.database-platform=org.hibernate.dialect.MySQL8Dialect )。 2. 在 @BeforeEach / @AfterEach 中重置静态状态。 3. 确保测试是独立的,或使用 @ResourceLock 等JUnit5机制控制并发。 |
一个关于 ArgumentCaptor 的实用例子 : 当你需要验证传递给Mock方法的参数对象内部的具体状态时,它非常有用。
@Test
void createUser_shouldCallRepositoryWithEncryptedPassword() {
// Arrange
UserSaveRequest request = new UserSaveRequest("user", "plainPassword");
ArgumentCaptor<User> userCaptor = ArgumentCaptor.forClass(User.class);
when(userRepository.save(any(User.class))).thenReturn(new User());
// Act
userService.createUser(request);
// Assert
verify(userRepository).save(userCaptor.capture());
User savedUser = userCaptor.getValue();
// 验证保存的用户对象密码是加密后的,而不是明文
assertThat(savedUser.getPassword()).isNotEqualTo("plainPassword");
assertThat(passwordEncoder.matches("plainPassword", savedUser.getPassword())).isTrue();
}
6. 测试资源管理与持续集成
最后,让测试套件能在团队和CI/CD流水线中稳定运行,还需要一些工程化考量。
6.1 测试数据管理
对于集成测试,准备测试数据是个麻烦事。除了使用 TestEntityManager 在方法内插入,还有更优雅的方式:
- 使用
@Sql注解 :直接在测试类或方法上执行SQL脚本。@Test @Sql(scripts = "/test-data/create-user.sql") @Sql(scripts = "/test-data/cleanup-user.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) void testWithSqlScript() { // 测试逻辑 } - 使用测试数据构建器(Test Data Builder) :利用建造者模式或对象母(Object Mother)模式,提供流畅的API来创建复杂的测试对象,避免测试代码中散落着大量的
new和setter。 - 使用专门的数据准备工具 :如DbUnit(较老)或通过Flyway/Liquibase维护测试专用的基线数据。
6.2 配置隔离与属性覆盖
测试环境配置必须与开发、生产隔离。
-
application-test.yml:在src/test/resources下创建,Spring Boot测试会自动加载application.yml和application-test.yml,后者优先级更高。在这里配置测试数据库连接、关闭不必要的组件(如Swagger UI扫描、定时任务)。# application-test.yml spring: datasource: url: jdbc:h2:mem:testdb;MODE=MySQL driver-class-name: org.h2.Driver jpa: hibernate: ddl-auto: create-drop show-sql: true properties: hibernate: dialect: org.hibernate.dialect.H2Dialect # 关闭缓存、邮件发送等 spring.cache.type: none spring.mail.host: localhost -
@TestPropertySource:在测试类上使用,直接覆盖特定属性,优先级最高。@SpringBootTest @TestPropertySource(properties = {"spring.datasource.url=jdbc:h2:mem:alternate"}) class PropertyOverrideTest { ... }
6.3 在CI/CD中运行测试
在Jenkins、GitLab CI等环境中,确保测试稳定运行:
- 内存管理 :Maven的
mvn test命令可能需要更多内存,特别是大型项目。在CI脚本中设置MAVEN_OPTS=-Xmx1g -XX:MaxMetaspaceSize=512m。 - 跳过非必要测试 :使用Maven的
-DskipITs跳过集成测试(如果分开),或用-Dtest=*UnitTest只运行单元测试。在流水线中,可以分阶段运行:先快速单元测试,通过后再运行较慢的集成测试。 - 测试报告 :确保配置了Surefire(单元测试)和Failsafe(集成测试)插件,并生成JUnit格式的XML报告(
target/surefire-reports/*.xml),以便CI工具(如Jenkins)收集和展示。 - 并行执行 :JUnit5支持并行运行测试。可以在
src/test/resources/junit-platform.properties中配置junit.jupiter.execution.parallel.enabled = true来加速测试套件运行。但需要仔细处理共享资源(如静态变量、测试数据库),避免竞态条件。
写测试不是负担,而是一种设计工具和安全网。一套好的测试实践,能迫使你写出更松耦合、更可测的代码,在长期迭代中极大地提升开发效率和系统可靠性。从今天起,尝试为你修改的每个Bug或添加的每个新功能,都配上相应的测试,你会发现代码质量和个人信心都会悄然增长。
更多推荐

所有评论(0)