Spring Boot 单元测试最佳实践:JUnit 5、Mockito、MockMvc 与测试切片
测试用例的价值不只是“提高覆盖率”,更重要的是让代码在持续迭代中保持可验证、可重构、可回归。Spring Boot 提供了完整的测试支持,但很多项目会把所有测试都写成 @SpringBootTest,导致启动慢、依赖重、数据互相污染,最终测试越来越难维护。
这篇文章整理 Spring Boot 测试的常用实践,重点区分单元测试、切片测试和集成测试,并说明 JUnit 5、Mockito、MockMvc、WebTestClient、Testcontainers 等工具的使用边界。
单元测试、切片测试、集成测试的区别
写测试前,先明确测试范围。
| 类型 | 目标 | 是否启动 Spring 容器 | 典型工具 |
|---|---|---|---|
| 单元测试 | 验证单个类或方法逻辑 | 否 | JUnit 5、Mockito |
| 切片测试 | 验证某一层组件 | 部分启动 | @WebMvcTest、@DataJpaTest、@JsonTest |
| 集成测试 | 验证完整应用上下文和外部依赖 | 是 | @SpringBootTest、Testcontainers |
经验建议:
- 能用普通 JUnit + Mockito 解决的,不要上
@SpringBootTest。 - Controller 层优先用
@WebMvcTest。 - Repository 层优先用
@DataJpaTest。 - 需要完整链路时再使用
@SpringBootTest。
spring-boot-starter-test 包含什么
通常只需要引入:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
它会集成 Spring Boot 测试常用能力,例如:
- JUnit Jupiter
- Spring Test / Spring Boot Test
- AssertJ
- Hamcrest
- Mockito
- JSONassert
- JsonPath
在较新的 Spring Boot 项目中,应优先使用 JUnit 5,而不是旧的 JUnit 4 @RunWith(SpringRunner.class) 写法。
JUnit 5 基础用法
JUnit 5 常用注解:
| 注解 | 说明 |
|---|---|
@Test |
测试方法 |
@BeforeEach |
每个测试前执行 |
@AfterEach |
每个测试后执行 |
@BeforeAll |
所有测试前执行 |
@AfterAll |
所有测试后执行 |
@DisplayName |
自定义测试名称 |
@ParameterizedTest |
参数化测试 |
示例:
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
class PriceCalculatorTest {
@Test
@DisplayName("会员价格应享受 8 折优惠")
void shouldCalculateMemberPrice() {
PriceCalculator calculator = new PriceCalculator();
int result = calculator.memberPrice(100);
assertThat(result).isEqualTo(80);
}
}
建议使用 AssertJ 的链式断言,表达力更强。
Mockito 单元测试
当被测类依赖其他组件时,可以使用 Mockito 隔离外部依赖。
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.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
@Mock
private UserRepository userRepository;
@InjectMocks
private UserService userService;
@Test
void shouldReturnUserName() {
when(userRepository.findNameById(1L)).thenReturn("Ryan");
String name = userService.getUserName(1L);
assertThat(name).isEqualTo("Ryan");
}
}
注意:
@Mock是 Mockito 的 Mock,不需要 Spring 容器。@InjectMocks会把 Mock 注入到被测对象。- 这种测试启动最快,适合业务逻辑类。
Spring Boot 测试切片
测试切片的目标是只启动某一层所需的 Spring 组件,降低上下文启动成本。
@WebMvcTest
适合测试 Controller 层。
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.test.web.servlet.MockMvc;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@WebMvcTest(UserController.class)
class UserControllerTest {
@Autowired
private MockMvc mockMvc;
@Test
void shouldGetUser() throws Exception {
mockMvc.perform(get("/users/1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(0));
}
}
如果 Controller 依赖 Service,需要提供 Mock Bean。Spring Boot 3.4 起 @MockBean 已被标记为 deprecated,后续应关注 @MockitoBean 等替代方案;如果项目仍在 Spring Boot 2.x / 3.3 及以前版本,可以继续按当前版本文档使用 @MockBean。
@DataJpaTest
适合测试 JPA Repository 层。
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import static org.assertj.core.api.Assertions.assertThat;
@DataJpaTest
class UserRepositoryTest {
@Autowired
private UserRepository userRepository;
@Test
void shouldSaveUser() {
User user = new User("Ryan");
userRepository.save(user);
assertThat(userRepository.findByName("Ryan")).isPresent();
}
}
默认情况下,@DataJpaTest 通常会在事务中运行测试,并在测试后回滚,适合保持数据隔离。
@JsonTest
适合测试 JSON 序列化和反序列化。
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.json.JsonTest;
import org.springframework.boot.test.json.JacksonTester;
import static org.assertj.core.api.Assertions.assertThat;
@JsonTest
class UserJsonTest {
@Autowired
private JacksonTester<UserDTO> json;
@Test
void shouldSerializeUser() throws Exception {
UserDTO user = new UserDTO(1L, "Ryan");
assertThat(json.write(user)).hasJsonPathStringValue("$.name");
assertThat(json.write(user)).extractingJsonPathStringValue("$.name").isEqualTo("Ryan");
}
}
@SpringBootTest 集成测试
@SpringBootTest 会启动完整 Spring 应用上下文,适合验证完整链路。
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
@SpringBootTest
@ActiveProfiles("test")
class ApplicationContextTest {
@Test
void contextLoads() {
}
}
如果需要启动真实 Web 服务,可以使用随机端口:
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.server.LocalServerPort;
import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class ServerTest {
@LocalServerPort
int port;
@Test
void shouldStartServer() {
assertThat(port).isGreaterThan(0);
}
}
常见 webEnvironment:
| 类型 | 说明 |
|---|---|
MOCK |
Mock Web 环境,不启动真实端口 |
RANDOM_PORT |
启动真实服务,使用随机端口 |
DEFINED_PORT |
使用配置中的固定端口 |
NONE |
非 Web 应用上下文 |
MockMvc 与 WebTestClient
MockMvc
适合 Servlet / Spring MVC 应用。
mockMvc.perform(get("/users/1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.id").value(1));
WebTestClient
适合 WebFlux,也可以用于部分集成测试场景。
webTestClient.get()
.uri("/users/1")
.exchange()
.expectStatus().isOk()
.expectBody()
.jsonPath("$.data.id").isEqualTo(1);
选择建议:
- Spring MVC:优先 MockMvc。
- Spring WebFlux:优先 WebTestClient。
- 真实 HTTP 端到端测试:可使用 TestRestTemplate、WebTestClient 或 RestClient 等。
JSONAssert 与 JsonPath
复杂 JSON 响应可以用 JSONAssert 或 JsonPath。
JSONAssert:
JSONAssert.assertEquals(
"{\"code\":0,\"msg\":\"success\"}",
actualJson,
JSONCompareMode.LENIENT
);
JsonPath:
Integer code = JsonPath.read(actualJson, "$.code");
assertThat(code).isZero();
实践建议:
- 不要对整段 JSON 做过度严格匹配。
- 只断言业务关键字段。
- 对列表结果要关注顺序是否有业务意义。
Testcontainers
如果测试依赖 MySQL、Redis、Kafka 等外部组件,使用本地共享环境容易导致测试不稳定。Testcontainers 可以在测试期间启动真实容器。
示例:
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.testcontainers.containers.MySQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
@Testcontainers
@SpringBootTest
class UserIntegrationTest {
@Container
static MySQLContainer<?> mysql = new MySQLContainer<>("mysql:8.0");
@Test
void shouldRunWithRealMysql() {
// integration test
}
}
Testcontainers 更适合集成测试,不建议把所有单元测试都容器化,否则测试会变慢。
降低测试耦合的实践
-
按测试层级选择工具
- 业务逻辑:JUnit + Mockito。
- Controller:
@WebMvcTest。 - Repository:
@DataJpaTest。 - 完整链路:
@SpringBootTest。
-
避免测试之间共享可变数据
- 每个测试自己准备数据。
- 使用事务回滚或清理脚本。
- 不依赖测试执行顺序。
-
不要过度 Mock
- Mock 太多会让测试只验证 Mock 行为,而不是业务行为。
- 核心领域逻辑尽量设计成纯 Java 类,减少框架依赖。
-
测试命名表达业务语义
- 推荐:
shouldReturnUserWhenUserExists。 - 避免:
test1、testGet。
- 推荐:
-
控制 Spring 上下文数量
- 不要随意组合大量不同配置,避免测试缓存失效导致整体变慢。
常见问题
1. 为什么测试很慢
常见原因:
- 大量使用
@SpringBootTest。 - 每个测试类都启动不同 Spring Context。
- 依赖外部数据库或中间件。
- 测试数据准备过重。
优化:
- 优先使用单元测试和切片测试。
- 复用测试配置。
- 使用 Testcontainers 时按需启用。
2. @Mock 和 @MockBean 有什么区别
@Mock:Mockito 提供,不进入 Spring 容器。@MockBean:Spring Boot 测试提供,会把 Mock 对象注册进 Spring 容器,替换原 Bean。
新版本中要关注 @MockBean 的替代方案和当前项目 Spring Boot 版本的官方文档。
3. 测试是否应该连接真实数据库
取决于测试目标:
- 单元测试:不需要。
- Repository 测试:可以使用内存数据库或 Testcontainers。
- 集成测试:建议使用尽可能接近生产的真实依赖,但要做好隔离。
总结
Spring Boot 测试的核心原则是:用最小成本验证最关键行为。
- 不要把所有测试都写成
@SpringBootTest。 - 单元测试优先 JUnit 5 + Mockito。
- Web 层优先
@WebMvcTest+ MockMvc。 - 数据层优先
@DataJpaTest。 - 外部依赖集成测试可使用 Testcontainers。
- 测试数据要隔离,测试命名要表达业务语义。
这样测试才能长期可维护,而不是随着项目复杂度上升逐渐失效。