Spring 测试框架
测试是软件开发中不可或缺的环节。Spring 提供了一套完整的测试支持框架,帮助开发者编写单元测试和集成测试。本章介绍 Spring 测试框架的核心概念和使用方法。
测试框架概述
为什么需要测试框架?
良好的测试能够保证代码质量,减少 Bug,提高代码的可维护性。但编写测试并不总是容易的事情,特别是当测试涉及 Spring 容器、数据库、Web 层等复杂组件时。
Spring 测试框架提供了以下能力:
依赖注入支持:在测试环境中使用 Spring 的依赖注入机制。
事务管理:测试方法可以自动回滚,保证测试之间相互隔离。
Mock 对象:提供 MockMvc 等 Mock 对象,简化 Web 层测试。
配置管理:支持测试专用的配置,与生产环境隔离。
上下文缓存:避免重复加载 Spring 上下文,提高测试效率。
测试类型
单元测试:测试单个类或方法,不依赖 Spring 容器。使用 JUnit、Mockito 等框架。
集成测试:测试多个组件的协作,需要 Spring 容器支持。使用 Spring Test 框架。
切片测试:测试应用的某一层,如 Web 层、数据层。使用 Spring Boot 的测试注解。
TestContext 框架
Spring TestContext 框架是 Spring 测试支持的核心,它提供了管理 Spring 上下文的统一方式。
核心组件
TestContextManager:测试上下文管理器,负责管理测试的生命周期。
TestContext:测试上下文,封装了测试类的上下文信息。
ContextLoader:上下文加载器,负责加载 Spring 配置。
TestExecutionListener:测试执行监听器,可以在测试的不同阶段执行自定义逻辑。
测试注解
Spring 提供了一组测试注解,用于配置测试环境:
// 指定配置类
@ContextConfiguration(classes = AppConfig.class)
// 指定 XML 配置文件
@ContextConfiguration(locations = "classpath:applicationContext.xml")
// 指定 Web 应用配置
@WebAppConfiguration
@ContextConfiguration(classes = WebConfig.class)
// 指定初始化器
@ContextConfiguration(initializers = CustomContextInitializer.class)
Spring Boot 测试
@SpringBootTest 注解
@SpringBootTest 是 Spring Boot 提供的集成测试注解,它会加载完整的 Spring 上下文:
@SpringBootTest
class ApplicationTests {
@Autowired
private UserService userService;
@Test
void testUserService() {
User user = userService.findById(1L);
assertNotNull(user);
}
}
@SpringBootTest 支持多种配置:
// 指定配置类
@SpringBootTest(classes = {AppConfig.class, TestConfig.class})
// 指定 Web 环境模式
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE)
// 指定配置属性
@SpringBootTest(properties = {
"spring.datasource.url=jdbc:h2:mem:testdb",
"spring.jpa.hibernate.ddl-auto=create-drop"
})
Web 环境模式说明:
| 模式 | 说明 |
|---|---|
| MOCK | 创建 Mock 的 Servlet 环境(默认) |
| RANDOM_PORT | 启动真实服务器,使用随机端口 |
| DEFINED_PORT | 启动真实服务器,使用指定端口 |
| NONE | 不创建 Web 环境 |
测试配置
测试类可以使用专用的配置文件 application-test.properties:
# application-test.properties
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driver-class-name=org.h2.Driver
spring.jpa.hibernate.ddl-auto=create-drop
通过 @ActiveProfiles 激活测试配置:
@SpringBootTest
@ActiveProfiles("test")
class UserServiceTests {
}
测试中的事务
默认情况下,测试方法会在事务中执行,执行完毕后自动回滚:
@SpringBootTest
class UserServiceTests {
@Autowired
private UserRepository userRepository;
@Test
void testInsert() {
User user = new User("test");
userRepository.save(user);
// 测试通过后,数据会自动回滚
// 不会影响其他测试
}
@Test
@Commit // 提交事务,不回滚
void testInsertWithCommit() {
User user = new User("test");
userRepository.save(user);
}
@Test
@Rollback(false) // 同 @Commit
void testInsertWithoutRollback() {
User user = new User("test");
userRepository.save(user);
}
}
切片测试
Spring Boot 提供了切片测试注解,只加载应用的部分组件,提高测试效率。
@WebMvcTest
测试 Spring MVC 控制器,只加载 Web 层组件:
@WebMvcTest(UserController.class)
class UserControllerTests {
@Autowired
private MockMvc mockMvc;
@MockBean
private UserService userService;
@Test
void testGetUser() throws Exception {
// 模拟 Service 返回数据
User user = new User(1L, "test");
when(userService.findById(1L)).thenReturn(user);
// 执行请求并验证
mockMvc.perform(get("/api/users/1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.id").value(1))
.andExpect(jsonPath("$.name").value("test"));
}
@Test
void testCreateUser() throws Exception {
User user = new User(null, "newuser");
User savedUser = new User(1L, "newuser");
when(userService.create(any())).thenReturn(savedUser);
mockMvc.perform(post("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"name\":\"newuser\"}"))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.id").value(1));
}
}
@WebMvcTest 会自动配置:
MockMvc@Controller、@ControllerAdvice、@JsonComponent等 Web 组件Converter、Formatter等类型转换组件MessageCodesResolver等验证相关组件
但不会配置:
@Component、@Service、@Repository等 Bean- 数据源、事务管理器等
@DataJpaTest
测试 JPA 组件,只加载数据访问层:
@DataJpaTest
class UserRepositoryTests {
@Autowired
private UserRepository userRepository;
@Autowired
private TestEntityManager entityManager;
@Test
void testFindById() {
// 准备测试数据
User user = new User("test");
entityManager.persist(user);
entityManager.flush();
// 执行测试
Optional<User> found = userRepository.findById(user.getId());
assertTrue(found.isPresent());
assertEquals("test", found.get().getName());
}
@Test
void testFindByName() {
User user = new User("test");
entityManager.persist(user);
User found = userRepository.findByName("test");
assertNotNull(found);
assertEquals("test", found.getName());
}
}
@DataJpaTest 会:
- 配置嵌入式数据库(H2、HSQL、Derby)
- 配置 Hibernate 和 JPA
- 配置
TestEntityManager - 扫描
@Entity类 - 扫描
Repository接口
@DataJdbcTest
测试 Spring Data JDBC:
@DataJdbcTest
class UserRepositoryTests {
@Autowired
private UserRepository userRepository;
@Test
void testSave() {
User user = new User("test");
User saved = userRepository.save(user);
assertNotNull(saved.getId());
}
}
@JdbcTest
测试纯 JDBC 操作:
@JdbcTest
class JdbcTests {
@Autowired
private JdbcTemplate jdbcTemplate;
@Test
void testQuery() {
jdbcTemplate.execute("CREATE TABLE test (id INT, name VARCHAR(50))");
jdbcTemplate.update("INSERT INTO test VALUES (1, 'test')");
Integer count = jdbcTemplate.queryForObject(
"SELECT COUNT(*) FROM test", Integer.class);
assertEquals(1, count);
}
}
@RedisTest
测试 Redis 操作:
@DataRedisTest
class RedisTests {
@Autowired
private StringRedisTemplate redisTemplate;
@Test
void testSet() {
redisTemplate.opsForValue().set("key", "value");
String value = redisTemplate.opsForValue().get("key");
assertEquals("value", value);
}
}
MockMvc 详解
MockMvc 是 Spring MVC 测试的核心工具,用于模拟 HTTP 请求。
基本使用
@SpringBootTest
@AutoConfigureMockMvc
class MockMvcTests {
@Autowired
private MockMvc mockMvc;
@Test
void testGet() throws Exception {
mockMvc.perform(get("/api/users"))
.andExpect(status().isOk())
.andExpect(content().contentType(MediaType.APPLICATION_JSON));
}
@Test
void testPost() throws Exception {
mockMvc.perform(post("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"name\":\"test\"}"))
.andExpect(status().isCreated());
}
@Test
void testPut() throws Exception {
mockMvc.perform(put("/api/users/1")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"name\":\"updated\"}"))
.andExpect(status().isOk());
}
@Test
void testDelete() throws Exception {
mockMvc.perform(delete("/api/users/1"))
.andExpect(status().isNoContent());
}
}
请求构建
MockMvc 提供了丰富的请求构建方法:
// GET 请求带参数
mockMvc.perform(get("/api/users")
.param("name", "test")
.param("page", "1")
.param("size", "10"))
// 带路径变量
mockMvc.perform(get("/api/users/{id}", 1))
// 带请求头
mockMvc.perform(get("/api/users")
.header("Authorization", "Bearer token")
.header("Accept", "application/json"))
// 带 Cookie
mockMvc.perform(get("/api/users")
.cookie(new Cookie("sessionId", "abc123")))
// 带请求体
mockMvc.perform(post("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"name\":\"test\",\"email\":\"[email protected]\"}"))
// 多部分文件上传
mockMvc.perform(multipart("/api/upload")
.file("file", fileBytes))
响应验证
验证响应状态和内容:
mockMvc.perform(get("/api/users/1"))
// 验证状态码
.andExpect(status().isOk())
.andExpect(status().is2xxSuccessful())
.andExpect(status().is(200))
// 验证响应头
.andExpect(header().string("Content-Type", "application/json"))
.andExpect(header().exists("X-Custom-Header"))
// 验证响应体
.andExpect(content().string("Hello"))
.andExpect(content().string(containsString("Hello")))
.andExpect(content().json("{\"name\":\"test\"}"))
// 验证 JSON
.andExpect(jsonPath("$.id").value(1))
.andExpect(jsonPath("$.name").value("test"))
.andExpect(jsonPath("$.email").exists())
.andExpect(jsonPath("$.age").isNumber())
.andExpect(jsonPath("$.active").isBoolean())
.andExpect(jsonPath("$.roles").isArray())
.andExpect(jsonPath("$.roles[0]").value("USER"))
.andExpect(jsonPath("$.roles", hasSize(2)))
.andExpect(jsonPath("$.address.city").value("Beijing"));
自定义 MockMvc 配置
@SpringBootTest
@AutoConfigureMockMvc
class CustomMockMvcTests {
@Autowired
private WebApplicationContext context;
private MockMvc mockMvc;
@BeforeEach
void setup() {
mockMvc = MockMvcBuilders
.webAppContextSetup(context)
.addFilters(new CharacterEncodingFilter("UTF-8", true))
.defaultRequest(get("/").accept(MediaType.APPLICATION_JSON))
.alwaysExpect(status().isOk())
.build();
}
}
Mock Bean
在测试中,我们经常需要模拟某些 Bean 的行为,隔离测试单元。
@MockBean
@MockBean 用于创建和注入 Mockito Mock 对象:
@SpringBootTest
class UserServiceTests {
@Autowired
private UserService userService;
@MockBean
private UserRepository userRepository;
@Test
void testFindById() {
// 定义 Mock 行为
User user = new User(1L, "test");
when(userRepository.findById(1L)).thenReturn(Optional.of(user));
// 执行测试
User result = userService.findById(1L);
// 验证结果
assertEquals("test", result.getName());
// 验证 Mock 方法被调用
verify(userRepository).findById(1L);
}
@Test
void testSave() {
User user = new User(null, "test");
User saved = new User(1L, "test");
when(userRepository.save(user)).thenReturn(saved);
User result = userService.create(user);
assertEquals(1L, result.getId());
}
}
@SpyBean
@SpyBean 创建真实的对象,但可以模拟部分方法:
@SpringBootTest
class OrderServiceTests {
@SpyBean
private EmailService emailService;
@Autowired
private OrderService orderService;
@Test
void testPlaceOrder() {
// 只模拟发送邮件方法
doNothing().when(emailService).sendEmail(anyString(), anyString());
orderService.placeOrder(new Order());
// 验证邮件发送方法被调用
verify(emailService).sendEmail(anyString(), anyString());
}
}
MockBean 与真实 Bean 的区别
| 特性 | @MockBean | @SpyBean | 真实 Bean |
|---|---|---|---|
| 创建方式 | 创建 Mock 对象 | 包装真实对象 | 使用真实对象 |
| 方法行为 | 默认返回 null | 调用真实方法 | 调用真实方法 |
| 可定制性 | 完全可定制 | 部分可定制 | 不可定制 |
| 适用场景 | 隔离依赖 | 部分模拟 | 集成测试 |
测试数据库
使用内存数据库
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
class UserRepositoryTests {
@Autowired
private UserRepository userRepository;
@Test
void testSave() {
User user = new User("test");
userRepository.save(user);
assertEquals(1, userRepository.count());
}
}
配置测试数据源:
# application-test.properties
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driver-class-name=org.h2.Driver
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
使用 Testcontainers
Testcontainers 可以为测试启动真实的数据库容器:
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>mysql</artifactId>
<scope>test</scope>
</dependency>
@SpringBootTest
@Testcontainers
class MySqlIntegrationTests {
@Container
private static final MySQLContainer<?> mysql = new MySQLContainer<>("mysql:8.0")
.withDatabaseName("testdb")
.withUsername("test")
.withPassword("test");
@DynamicPropertySource
static void configureProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", mysql::getJdbcUrl);
registry.add("spring.datasource.username", mysql::getUsername);
registry.add("spring.datasource.password", mysql::getPassword);
}
@Test
void testDatabaseConnection() {
// 使用真实的 MySQL 数据库进行测试
}
}
初始化测试数据
使用 @Sql 注解执行 SQL 脚本:
@SpringBootTest
@Sql(scripts = "/data.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
@Sql(scripts = "/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
class UserServiceIntegrationTests {
@Test
void testFindAll() {
// data.sql 已执行,数据库中有测试数据
}
}
测试安全
测试受保护的接口
@SpringBootTest
@AutoConfigureMockMvc
class SecurityTests {
@Autowired
private MockMvc mockMvc;
@Test
void testUnauthenticatedAccess() throws Exception {
mockMvc.perform(get("/api/private"))
.andExpect(status().isUnauthorized());
}
@Test
@WithMockUser(username = "user", roles = {"USER"})
void testAuthenticatedAccess() throws Exception {
mockMvc.perform(get("/api/user"))
.andExpect(status().isOk());
}
@Test
@WithMockUser(username = "admin", roles = {"ADMIN"})
void testAdminAccess() throws Exception {
mockMvc.perform(get("/api/admin"))
.andExpect(status().isOk());
}
@Test
@WithUserDetails("testuser")
void testWithRealUser() throws Exception {
// 使用数据库中真实的用户
mockMvc.perform(get("/api/user"))
.andExpect(status().isOk());
}
}
安全测试注解:
| 注解 | 说明 |
|---|---|
| @WithMockUser | 使用模拟用户 |
| @WithUserDetails | 使用真实的 UserDetails |
| @WithAnonymousUser | 使用匿名用户 |
| @WithSecurityContext | 自定义 SecurityContext |
测试事件
测试 Spring 事件发布和监听:
@SpringBootTest
class EventTests {
@Autowired
private ApplicationEventPublisher eventPublisher;
@Autowired
private ApplicationContext applicationContext;
@Test
void testEventPublishing() {
// 创建事件监听器
CountDownLatch latch = new CountDownLatch(1);
AtomicReference<UserCreatedEvent> eventRef = new AtomicReference<>();
ApplicationListener<UserCreatedEvent> listener = event -> {
eventRef.set(event);
latch.countDown();
};
applicationContext.addApplicationListener(listener);
// 发布事件
UserCreatedEvent event = new UserCreatedEvent(this, 1L);
eventPublisher.publishEvent(event);
// 等待事件处理完成
await().atMost(1, TimeUnit.SECONDS).until(() -> latch.getCount() == 0);
assertEquals(1L, eventRef.get().getUserId());
}
}
测试最佳实践
测试命名规范
测试类和方法应该有清晰的命名:
// 测试类命名:被测试类名 + Tests
class UserServiceTests {
// 测试方法命名:should_ExpectedBehavior_When_Condition
@Test
void should_ReturnUser_When_UserExists() {
}
@Test
void should_ThrowException_When_UserNotFound() {
}
@Test
void should_CreateUser_When_ValidInput() {
}
}
测试结构
遵循 Given-When-Then 模式:
@Test
void should_ReturnUser_When_UserExists() {
// Given(准备)
Long userId = 1L;
User expectedUser = new User(userId, "test");
when(userRepository.findById(userId)).thenReturn(Optional.of(expectedUser));
// When(执行)
User result = userService.findById(userId);
// Then(验证)
assertEquals(expectedUser, result);
verify(userRepository).findById(userId);
}
测试隔离
每个测试应该相互独立:
@SpringBootTest
class IsolatedTests {
@Autowired
private UserRepository userRepository;
@BeforeEach
void setUp() {
// 清理数据
userRepository.deleteAll();
}
@Test
void test1() {
// 使用干净的数据
}
@Test
void test2() {
// 使用干净的数据,不受 test1 影响
}
}
避免过度 Mock
不是所有依赖都需要 Mock:
// 不好的做法:Mock 所有依赖
@ExtendWith(MockitoExtension.class)
class BadExampleTests {
@Mock private UserRepository repository;
@Mock private EmailService emailService;
@Mock private CacheService cacheService;
// ... 过度 Mock
}
// 好的做法:只 Mock 外部依赖
@SpringBootTest
class GoodExampleTests {
@MockBean private EmailService emailService; // 外部服务,需要 Mock
@Autowired private UserRepository repository; // 数据层,使用真实的
}
常用测试注解汇总
| 注解 | 说明 |
|---|---|
| @SpringBootTest | 完整的 Spring Boot 测试 |
| @WebMvcTest | MVC 层切片测试 |
| @DataJpaTest | JPA 层切片测试 |
| @DataJdbcTest | JDBC 层切片测试 |
| @JdbcTest | 纯 JDBC 测试 |
| @DataRedisTest | Redis 层切片测试 |
| @MockBean | 创建 Mock Bean |
| @SpyBean | 创建 Spy Bean |
| @AutoConfigureMockMvc | 自动配置 MockMvc |
| @AutoConfigureTestDatabase | 自动配置测试数据库 |
| @ActiveProfiles | 激活指定的 Profile |
| @TestPropertySource | 指定测试配置文件 |
| @Sql | 执行 SQL 脚本 |
| @WithMockUser | 模拟认证用户 |
小结
本章详细介绍了 Spring 测试框架:
- TestContext 框架:Spring 测试的核心架构
- Spring Boot 测试:
@SpringBootTest和测试配置 - 切片测试:
@WebMvcTest、@DataJpaTest等 - MockMvc:Web 层测试的核心工具
- Mock Bean:
@MockBean和@SpyBean的使用 - 测试数据库:内存数据库和 Testcontainers
- 测试安全:安全相关的测试方法
- 最佳实践:命名规范、测试结构、测试隔离
良好的测试是高质量代码的保障,掌握 Spring 测试框架能够帮助我们编写可靠的测试代码。