前言
集成测试验证多个组件协同工作是否正常。Spring Boot 提供了@SpringBootTest 等强大的集成测试支持。本文将介绍 Spring Boot 集成测试的完整方案。
基础集成测试
1. @SpringBootTest
@SpringBootTest
@AutoConfigureMockMvc
class UserControllerIntegrationTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private UserRepository userRepository;
@BeforeEach
void setUp() {
userRepository.deleteAll();
}
@Test
void testCreateUser() throws Exception {
String requestBody = """
{
"username": "test",
"email": "test@example.com"
}
""";
mockMvc.perform(post("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.content(requestBody))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200))
.andExpect(jsonPath("$.data.username").value("test"));
// 验证数据库
Optional<User> user = userRepository.findByUsername("test");
assertTrue(user.isPresent());
}
@Test
void testGetUser() throws Exception {
User user = new User();
user.setUsername("test");
user.setEmail("test@example.com");
userRepository.save(user);
mockMvc.perform(get("/api/users/" + user.getId()))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.id").value(user.getId()));
}
}
2. 测试配置
@SpringBootTest
@ActiveProfiles("test")
class IntegrationTest {
@TestConfiguration
static class TestConfig {
@Bean
public ExternalService externalService() {
return mock(ExternalService.class);
}
}
}
# application-test.yml
spring:
datasource:
url: jdbc:h2:mem:testdb
username: sa
password:
jpa:
hibernate:
ddl-auto: create-drop
mail:
host: localhost
redis:
host: localhost
3. 测试事务
@SpringBootTest
@Transactional
class TransactionalTest {
@Autowired
private UserRepository userRepository;
@Test
void testRollback() {
User user = new User();
user.setUsername("test");
userRepository.save(user);
// 测试完成后自动回滚
assertEquals(1, userRepository.count());
}
@Test
void testMultipleOperations() {
User user1 = new User();
user1.setUsername("user1");
userRepository.save(user1);
User user2 = new User();
user2.setUsername("user2");
userRepository.save(user2);
assertEquals(2, userRepository.count());
}
}
TestContainers
1. 添加依赖
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
<version>1.19.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>mysql</artifactId>
<version>1.19.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</artifactId>
<version>1.19.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>redis</artifactId>
<version>1.19.3</version>
<scope>test</scope>
</dependency>
2. MySQL TestContainer
@SpringBootTest
class MySQLContainerTest {
@Container
static MySQLContainer<?> mysql = new MySQLContainer<>("mysql:8.0")
.withDatabaseName("testdb")
.withUsername("test")
.withPassword("test");
@DynamicPropertySource
static void configureTestProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", mysql::getJdbcUrl);
registry.add("spring.datasource.username", mysql::getUsername);
registry.add("spring.datasource.password", mysql::getPassword);
registry.add("spring.datasource.driver-class-name", mysql::getDriverClassName);
}
@Autowired
private UserRepository userRepository;
@Test
void testWithMySQL() {
User user = new User();
user.setUsername("test");
userRepository.save(user);
Optional<User> found = userRepository.findByUsername("test");
assertTrue(found.isPresent());
}
}
3. PostgreSQL TestContainer
@SpringBootTest
class PostgreSQLContainerTest {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15")
.withDatabaseName("testdb")
.withUsername("test")
.withPassword("test");
@DynamicPropertySource
static void configureProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", postgres::getJdbcUrl);
registry.add("spring.datasource.username", postgres::getUsername);
registry.add("spring.datasource.password", postgres::getPassword);
}
@Test
void testWithPostgreSQL() {
// 测试逻辑
}
}
4. Redis TestContainer
@SpringBootTest
class RedisContainerTest {
@Container
static RedisContainer<?> redis = new RedisContainer<>("redis:7")
.withExposedPorts(6379);
@DynamicPropertySource
static void configureProperties(DynamicPropertyRegistry registry) {
registry.add("spring.redis.host", redis::getContainerIpAddress);
registry.add("spring.redis.port", () -> redis.getMappedPort(6379));
}
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Test
void testWithRedis() {
redisTemplate.opsForValue().set("key", "value");
String value = (String) redisTemplate.opsForValue().get("key");
assertEquals("value", value);
}
}
5. 多容器组合
@SpringBootTest
class MultiContainerTest {
@Container
static MySQLContainer<?> mysql = new MySQLContainer<>("mysql:8.0")
.withDatabaseName("testdb")
.withUsername("test")
.withPassword("test");
@Container
static RedisContainer<?> redis = new RedisContainer<>("redis:7");
@DynamicPropertySource
static void configureProperties(DynamicPropertyRegistry registry) {
// MySQL
registry.add("spring.datasource.url", mysql::getJdbcUrl);
registry.add("spring.datasource.username", mysql::getUsername);
registry.add("spring.datasource.password", mysql::getPassword);
// Redis
registry.add("spring.redis.host", redis::getContainerIpAddress);
registry.add("spring.redis.port", () -> redis.getMappedPort(6379));
}
@Test
void testWithMultiContainer() {
// 测试 MySQL 和 Redis 集成
}
}
6. JUnit 5 扩展
@Testcontainers
@SpringBootTest
class TestcontainersExtensionTest {
@Container
static MySQLContainer<?> mysql = new MySQLContainer<>("mysql:8.0");
@DynamicPropertySource
static void configureProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", mysql::getJdbcUrl);
}
@Test
void test() {
// 测试逻辑
}
}
Mock 外部服务
1. WireMock HTTP Mock
<dependency>
<groupId>com.github.tomakehurst</groupId>
<artifactId>wiremock</artifactId>
<version>3.0.1</version>
<scope>test</scope>
</dependency>
@SpringBootTest
@AutoConfigureWireMock(port = 0)
class WireMockTest {
@Autowired
private ExternalService externalService;
@Test
void testWithWireMock() {
// 配置 Mock
stubFor(get(urlEqualTo("/api/users/1"))
.willReturn(aResponse()
.withStatus(200)
.withHeader("Content-Type", "application/json")
.withBody("""
{
"id": 1,
"name": "test"
}
""")));
// 执行测试
UserInfo userInfo = externalService.getUserInfo(1L);
assertEquals(1L, userInfo.getId());
assertEquals("test", userInfo.getName());
}
}
2. MockServer
<dependency>
<groupId>org.mock-server</groupId>
<artifactId>mockserver-netty</artifactId>
<version>5.15.0</version>
<scope>test</scope>
</dependency>
@SpringBootTest
class MockServerTest {
private MockServerClient mockServerClient;
@BeforeEach
void setUp() {
mockServerClient = new MockServerClient("localhost", 1080);
}
@Test
void testWithMockServer() {
mockServerClient
.when(request()
.withMethod("GET")
.withPath("/api/users/1"))
.respond(response()
.withStatusCode(200)
.withBody("""
{"id":1,"name":"test"}
"""));
// 测试逻辑
}
}
3. @MockBean
@SpringBootTest
class MockBeanTest {
@MockBean
private ExternalService externalService;
@Autowired
private UserService userService;
@Test
void testWithMockBean() {
when(externalService.getUserInfo(anyLong()))
.thenReturn(new UserInfo(1L, "test"));
UserDTO user = userService.getUserWithExternalInfo(1L);
assertEquals("test", user.getExternalName());
verify(externalService).getUserInfo(1L);
}
}
端到端测试
1. 完整流程测试
@SpringBootTest
@AutoConfigureMockMvc
class E2ETest {
@Autowired
private MockMvc mockMvc;
@Autowired
private ObjectMapper objectMapper;
@Test
void testUserRegistrationFlow() throws Exception {
// 1. 注册用户
String registerRequest = """
{
"username": "newuser",
"email": "newuser@example.com",
"password": "password123"
}
""";
mockMvc.perform(post("/api/auth/register")
.contentType(MediaType.APPLICATION_JSON)
.content(registerRequest))
.andExpect(status().isOk());
// 2. 登录
String loginRequest = """
{
"username": "newuser",
"password": "password123"
}
""";
String token = mockMvc.perform(post("/api/auth/login")
.contentType(MediaType.APPLICATION_JSON)
.content(loginRequest))
.andExpect(status().isOk())
.andReturn()
.getResponse()
.getContentAsString();
// 3. 使用 Token 访问受保护接口
mockMvc.perform(get("/api/users/me")
.header("Authorization", "Bearer " + token))
.andExpect(status().isOk());
}
}
2. 数据准备
@SpringBootTest
@AutoConfigureMockMvc
@DataSql(scripts = "/test-data.sql")
class DataSqlTest {
@Autowired
private MockMvc mockMvc;
@Test
void testWithDataSql() throws Exception {
// 使用 SQL 脚本准备数据
mockMvc.perform(get("/api/users"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.length()").value(5));
}
}
-- test-data.sql
INSERT INTO users (username, email, created_at) VALUES
('user1', 'user1@example.com', NOW()),
('user2', 'user2@example.com', NOW()),
('user3', 'user3@example.com', NOW());
3. TestExecutionListener
@SpringBootTest
@TestExecutionListeners(
value = {DependencyInjectionTestExecutionListener.class},
mergeMode = TestExecutionListeners.MergeMode.MERGE_WITH_DEFAULTS
)
class CustomListenerTest {
@Autowired
private TestDataInitializer testDataInitializer;
@Test
void testWithCustomData() {
// 使用自定义数据初始化器准备数据
}
}
最佳实践
1. 测试分层
// ✅ 推荐 - 分层测试
@Nested
class UnitTests {
// 单元测试
}
@Nested
class IntegrationTests {
// 集成测试
}
@Nested
class E2ETests {
// 端到端测试
}
// ❌ 不推荐 - 混合测试
2. 测试数据管理
@SpringBootTest
class TestDataManagementTest {
@Autowired
private TestDataInitializer testDataInitializer;
@BeforeEach
void setUp() {
// 每个测试前清理数据
testDataInitializer.cleanData();
}
@AfterEach
void tearDown() {
// 每个测试后清理数据
testDataInitializer.cleanData();
}
@Test
void testWithData() {
// 准备测试数据
User user = testDataInitializer.createUser("test");
// 执行测试
// ...
}
}
3. 并行执行
<!-- pom.xml -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<parallel>classes</parallel>
<threadCount>4</threadCount>
</configuration>
</plugin>
4. 测试报告
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<reportsDirectory>${project.build.directory}/surefire-reports</reportsDirectory>
</configuration>
</plugin>
5. Docker Compose 测试
# docker-compose.test.yml
version: '3.8'
services:
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: test
MYSQL_DATABASE: testdb
redis:
image: redis:7
app:
build: .
environment:
- SPRING_PROFILES_ACTIVE=test
depends_on:
- mysql
- redis
@SpringBootTest(properties = {
"spring.docker.compose.lifecycle-management=start_only"
})
class DockerComposeTest {
@Test
void testWithDockerCompose() {
// 使用 Docker Compose 启动的服务
}
}
总结
集成测试要点:
- ✅ @SpringBootTest - 完整应用上下文
- ✅ TestContainers - 真实数据库测试
- ✅ Mock 外部服务 - WireMock、MockServer
- ✅ 端到端测试 - 完整流程验证
- ✅ 最佳实践 - 分层、数据管理、并行执行
集成测试是系统质量的重要保障。