Skip to content
清晨的一缕阳光
返回

Spring Boot 集成测试实战

前言

集成测试验证多个组件协同工作是否正常。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 启动的服务
    }
}

总结

集成测试要点:

集成测试是系统质量的重要保障。


分享这篇文章到:

上一篇文章
Spring Boot 拦截器与 AOP
下一篇文章
K8s 部署实战