Spring Boot 单元测试完全指南
Spring Boot 提供了完整的测试支持,核心是 spring-boot-starter-test,它整合了 JUnit、Mockito、AssertJ、Hamcrest、JsonPath 等主流测试库。
一、依赖引入
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
这个依赖默认提供了以下测试框架:
- JUnit 5:测试运行引擎
- Mockito:Mock 框架
- AssertJ:流式断言
- Hamcrest:匹配器
- JsonPath:JSON 断言
- Spring Test:Spring 测试支持
二、测试注解
@SpringBootTest
加载完整的 Spring 应用上下文,用于集成测试。默认情况下,它会查找 @SpringBootApplication 启动类并加载所有配置。
@SpringBootTest
public class UserServiceTest {
@Autowired
private UserService userService;
@Test
public void testGetUser() {
User user = userService.getById(1L);
assertThat(user).isNotNull();
}
}
@SpringBootTest 支持通过 webEnvironment 属性控制 Web 环境:
| webEnvironment 值 | 说明 |
|---|---|
MOCK | 默认值,加载模拟的 Servlet 环境,不启动真实 Web 服务器 |
RANDOM_PORT | 启动内嵌 Web 服务器,使用随机端口 |
DEFINED_PORT | 启动内嵌 Web 服务器,使用配置的端口 |
NONE | 不加载 Web 环境,适用于非 Web 测试 |
@Test
JUnit 5 的测试方法注解,标记一个方法为测试用例。
@Test
public void testAddUser() {
// 测试逻辑
}
三、Web 层切片测试
@WebMvcTest
只加载 Web 层组件(Controller、HandlerInterceptor、WebMvcConfigurer),不加载 Service、Mapper 等业务组件。测试 Controller 层性能更好,启动更快。
@WebMvcTest(UserController.class)
public class UserControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private UserService userService;
@Test
public void testGetUser() throws Exception {
User mockUser = new User(1L, "张三");
when(userService.getById(1L)).thenReturn(mockUser);
mockMvc.perform(get("/user/1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.name").value("张三"));
}
}
@WebMvcTest 只加载指定的 Controller,Service 层需要配合 @MockBean 模拟依赖。如果 Controller 依赖其他组件(如拦截器、参数解析器),需要额外配置或使用 @Import 导入。
MockMvc
MockMvc 是 Spring 提供的模拟 HTTP 请求的工具,无需启动真实 Web 服务器就能测试 Controller 的行为。
常用方法:
mockMvc.perform(get("/user/1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.name").value("张三"))
.andExpect(jsonPath("$.age").value(18))
.andDo(print());
四、数据访问层切片测试
@DataJpaTest
只加载 JPA 相关组件(Repository、EntityManager、DataSource),默认使用嵌入式内存数据库(H2 或 Derby),测试数据不会污染真实数据库。
@DataJpaTest
public class UserRepositoryTest {
@Autowired
private UserRepository userRepository;
@Test
public void testSaveUser() {
User user = new User("张三");
User saved = userRepository.save(user);
assertThat(saved.getId()).isNotNull();
}
}
@MybatisTest
MyBatis 生态的切片测试注解,只加载 MyBatis 相关组件(Mapper、SqlSessionFactory)。同样默认使用内存数据库。
@MybatisTest
public class UserMapperTest {
@Autowired
private UserMapper userMapper;
@Test
public void testSelectById() {
User user = userMapper.selectById(1L);
assertThat(user).isNotNull();
}
}
五、Mock 相关注解
@MockBean
在 Spring 容器中创建一个 Mockito Mock 对象,并替换同类型的 Bean。常用于隔离外部依赖。
@SpringBootTest
public class UserServiceTest {
@MockBean
private UserMapper userMapper;
@Autowired
private UserService userService;
@Test
public void testGetUser() {
User mockUser = new User(1L, "张三");
when(userMapper.selectById(1L)).thenReturn(mockUser);
User result = userService.getById(1L);
assertThat(result.getName()).isEqualTo("张三");
}
}
@SpyBean
创建 Mockito Spy 对象,保留真实对象的部分行为,可以部分 Mock。
@SpyBean
private UserService userService;
@Test
public void testPartialMock() {
// 真实调用会执行真实逻辑,只有 doReturn 的部分被 Mock
doReturn("Mocked Name").when(userService).getName();
}
@MockBean 和 @SpyBean 的区别在于:@MockBean 完全模拟对象,所有方法调用都返回默认值或需要显式 stubbing;@SpyBean 保留真实对象的行为,只对显式 stubbing 的方法返回 Mock 值。
六、配置覆盖
@TestPropertySource
在测试类上指定额外的属性配置,覆盖主配置文件中的属性。
@SpringBootTest
@TestPropertySource(properties = {
"app.name=test-app",
"app.timeout=5000"
})
public class ConfigTest {
@Value("${app.name}")
private String appName;
@Test
public void testProperties() {
assertThat(appName).isEqualTo("test-app");
}
}
@TestConfiguration
在测试类中定义额外的配置,仅在测试环境中生效,不会影响主应用。
@TestConfiguration
public class TestConfig {
@Bean
public UserService mockUserService() {
return mock(UserService.class);
}
}
@SpringBootTest
@Import(TestConfig.class)
public class ServiceTest {
@Autowired
private UserService userService;
}
七、事务与回滚
默认情况下,@DataJpaTest 和 @MybatisTest 会在每个测试方法结束后自动回滚事务,测试数据不会持久化到数据库。
@SpringBootTest 默认不自动回滚,需要手动添加 @Transactional 和 @Rollback:
@SpringBootTest
@Transactional
@Rollback
public class UserServiceTest {
@Test
public void testInsertUser() {
userService.insert(new User("张三"));
// 测试结束后自动回滚
}
}
八、断言工具
AssertJ
spring-boot-starter-test 默认依赖 AssertJ,提供了流式断言 API:
// 基本断言
assertThat(user.getName()).isEqualTo("张三");
assertThat(user.getAge()).isGreaterThan(18);
// 集合断言
assertThat(userList).hasSize(3);
assertThat(userList).extracting(User::getName).contains("张三", "李四");
// 异常断言
assertThatThrownBy(() -> userService.getById(null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("id不能为空");
JsonPath
用于断言 JSON 响应:
mockMvc.perform(get("/user/1"))
.andExpect(jsonPath("$.id").value(1))
.andExpect(jsonPath("$.name").value("张三"))
.andExpect(jsonPath("$.orders.length()").value(3));
九、上下文缓存
Spring 测试框架会缓存 ApplicationContext。如果多个测试类有相同的配置,它们会复用同一个上下文,避免重复加载,加快测试执行速度。
// Test1 加载了上下文
@SpringBootTest
class Test1 { }
// Test2 复用 Test1 的上下文(配置相同)
@SpringBootTest
class Test2 { }
如果某个测试类使用了不同的配置(如 @MockBean 或 @TestPropertySource),会创建新的上下文,缓存失效。
// 上下文 A:使用 @MockBean
@SpringBootTest
class TestWithMock { }
// 上下文 B:没有 @MockBean(不同配置,不能复用)
@SpringBootTest
class TestWithoutMock { }
十、最佳实践
- 使用切片测试(
@WebMvcTest、@DataJpaTest)代替完整@SpringBootTest,启动速度更快,隔离性更好 - 单元测试用
@MockBean模拟外部依赖,减少测试对环境的依赖 - 在
@DataJpaTest和@MybatisTest中,对查询逻辑进行验证,确保 SQL 正确性 @WebMvcTest测试 Controller 时验证 HTTP 状态码、响应体结构、JSON 字段类型,确保接口契约正确- 用
@TestPropertySource覆盖敏感配置(如第三方 API Key、数据库连接串),避免测试代码泄露敏感信息 - 保持测试独立性,每个测试方法执行后恢复上下文状态,避免测试间依赖执行顺序
@BeforeEach和@AfterEach用于准备测试数据和清理资源- 测试方法命名清晰,如
shouldReturnUserWhenIdExists比testGetUser更能表达意图 - 使用
@DisplayName为测试方法添加中文或可读性更强的描述 - 生产代码和测试代码保持相同包结构,方便访问包级私有成员