跳到主要内容

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 组件
  • ConverterFormatter 等类型转换组件
  • 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 测试
@WebMvcTestMVC 层切片测试
@DataJpaTestJPA 层切片测试
@DataJdbcTestJDBC 层切片测试
@JdbcTest纯 JDBC 测试
@DataRedisTestRedis 层切片测试
@MockBean创建 Mock Bean
@SpyBean创建 Spy Bean
@AutoConfigureMockMvc自动配置 MockMvc
@AutoConfigureTestDatabase自动配置测试数据库
@ActiveProfiles激活指定的 Profile
@TestPropertySource指定测试配置文件
@Sql执行 SQL 脚本
@WithMockUser模拟认证用户

小结

本章详细介绍了 Spring 测试框架:

  1. TestContext 框架:Spring 测试的核心架构
  2. Spring Boot 测试@SpringBootTest 和测试配置
  3. 切片测试@WebMvcTest@DataJpaTest
  4. MockMvc:Web 层测试的核心工具
  5. Mock Bean@MockBean@SpyBean 的使用
  6. 测试数据库:内存数据库和 Testcontainers
  7. 测试安全:安全相关的测试方法
  8. 最佳实践:命名规范、测试结构、测试隔离

良好的测试是高质量代码的保障,掌握 Spring 测试框架能够帮助我们编写可靠的测试代码。