Spring Boot测试全景指南:JUnit 5 + Testcontainers实现单元与集成测试

发布于:2025-07-16 ⋅ 阅读:(16) ⋅ 点赞:(0)

Spring Boot测试全景指南:JUnit 5 + Testcontainers实现单元与集成测试

测试是保障软件质量的核心环节。本文将深入探讨Spring Boot中如何利用JUnit 5和Testcontainers构建现代化的测试体系,涵盖从单元测试到集成测试的全流程解决方案。

一、测试金字塔:构建健康测试体系

​测试策略对比​​:

测试类型 测试范围 执行速度 可靠性 维护成本
单元测试 单个类/方法 毫秒级
集成测试 模块/服务 秒级
E2E测试 完整系统 分钟级

二、JUnit 5核心特性解析

1. 注解体系进化

2. 参数化测试示例

@ParameterizedTest
@CsvSource({
    "1, true",
    "2, false",
    "3, true"
})
@DisplayName("订单有效性验证")
void testOrderValidation(int orderId, boolean expected) {
    Order order = orderRepository.findById(orderId);
    assertEquals(expected, validator.isValid(order));
}

3. 动态测试生成

@TestFactory
Stream<DynamicTest> dynamicTests() {
    return IntStream.range(1, 6)
        .mapToObj(id -> DynamicTest.dynamicTest(
            "测试订单 #" + id, 
            () -> {
                Order order = orderService.getOrder(id);
                assertNotNull(order);
            }
        ));
}

三、单元测试实战:Mocking与隔离

1. Mockito高级用法

@ExtendWith(MockitoExtension.class)
class PaymentServiceTest {
    
    @Mock
    private PaymentGateway gateway;
    
    @InjectMocks
    private PaymentService paymentService;
    
    @Test
    void shouldProcessPaymentSuccessfully() {
        // 配置模拟行为
        when(gateway.process(any(PaymentRequest.class)))
            .thenReturn(new PaymentResponse(Status.SUCCESS));
        
        // 执行测试
        PaymentResult result = paymentService.executePayment(100.0);
        
        // 验证结果
        assertTrue(result.isSuccess());
        
        // 验证交互
        verify(gateway, times(1)).process(any());
    }
    
    @Test
    void shouldHandleGatewayFailure() {
        when(gateway.process(any()))
            .thenThrow(new PaymentException("网关超时"));
            
        assertThrows(PaymentException.class, 
            () -> paymentService.executePayment(50.0));
    }
}

2. 测试覆盖度分析

使用Jacoco配置:

<plugin>
    <groupId>org.jacoco</groupId>
    <artifactId>jacoco-maven-plugin</artifactId>
    <executions>
        <execution>
            <goals>
                <goal>prepare-agent</goal>
            </goals>
        </execution>
        <execution>
            <id>report</id>
            <phase>test</phase>
            <goals>
                <goal>report</goal>
            </goals>
        </execution>
    </executions>
    <configuration>
        <excludes>
            <exclude>**/dto/**</exclude>
            <exclude>**/config/**</exclude>
        </excludes>
    </configuration>
</plugin>

四、Testcontainers集成测试实战

1. Testcontainers核心优势

2. PostgreSQL集成测试配置

@Testcontainers
@SpringBootTest
@ActiveProfiles("test")
public class UserRepositoryIntegrationTest {
    
    @Container
    static PostgreSQLContainer<?> postgres = 
        new PostgreSQLContainer<>("postgres:15-alpine")
            .withDatabaseName("testdb")
            .withUsername("test")
            .withPassword("test");
    
    @DynamicPropertySource
    static void registerProperties(DynamicPropertyRegistry registry) {
        registry.add("spring.datasource.url", postgres::getJdbcUrl);
        registry.add("spring.datasource.username", postgres::getUsername);
        registry.add("spring.datasource.password", postgres::getPassword);
    }
    
    @Autowired
    private UserRepository userRepository;
    
    @Test
    void shouldSaveAndRetrieveUser() {
        User user = new User("test@example.com", "password");
        userRepository.save(user);
        
        User found = userRepository.findByEmail("test@example.com");
        assertNotNull(found);
        assertEquals("password", found.getPassword());
    }
}

3. 多容器协作测试

@Container
static DockerComposeContainer<?> environment =
    new DockerComposeContainer<>(new File("docker-compose-test.yml"))
        .withExposedService("db", 5432)
        .withExposedService("redis", 6379);

@Test
void testMultiContainerIntegration() {
    String dbUrl = environment.getServiceHost("db", 5432);
    String redisUrl = environment.getServiceHost("redis", 6379);
    
    // 测试数据库和Redis交互
    userService.cacheUserProfile(dbUrl, redisUrl);
}

五、测试切片技术:精准测试策略

1. 常用测试切片注解

2. WebMvcTest切片示例

@WebMvcTest(UserController.class)
@Import(SecurityConfig.class)
class UserControllerTest {
    
    @Autowired
    private MockMvc mockMvc;
    
    @MockBean
    private UserService userService;
    
    @Test
    void shouldReturnUserDetails() throws Exception {
        given(userService.getUser(1L))
            .willReturn(new UserDTO("test@example.com"));
        
        mockMvc.perform(get("/users/1"))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.email").value("test@example.com"));
    }
}

3. DataJpaTest切片示例

@DataJpaTest
@AutoConfigureTestDatabase(replace = Replace.NONE)
@Import(TestConfig.class)
class UserRepositoryTest {
    
    @Autowired
    private TestEntityManager entityManager;
    
    @Autowired
    private UserRepository repository;
    
    @Test
    void shouldFindByEmail() {
        User user = new User("test@example.com", "password");
        entityManager.persist(user);
        entityManager.flush();
        
        User found = repository.findByEmail(user.getEmail());
        assertThat(found.getEmail()).isEqualTo(user.getEmail());
    }
}

六、测试配置管理策略

1. 多环境配置方案

2. 测试专用配置示例

# application-test.yml
spring:
  datasource:
    url: jdbc:h2:mem:testdb
    driver-class-name: org.h2.Driver
  jpa:
    database-platform: org.hibernate.dialect.H2Dialect
    hibernate:
      ddl-auto: create-drop

3. 动态属性覆盖

@TestPropertySource(properties = {
    "spring.datasource.url=jdbc:h2:mem:tempdb",
    "feature.flag.new-payment=true"
})
public class PaymentServiceTest {
    // 测试将使用临时内存数据库
}

七、高级测试场景解决方案

1. 数据库版本控制测试

@SpringBootTest
@AutoConfigureTestDatabase(replace = Replace.NONE)
@Testcontainers
public class FlywayMigrationTest {
    
    @Container
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15");
    
    @Autowired
    private DataSource dataSource;
    
    @Test
    void shouldApplyAllMigrations() {
        Flyway flyway = Flyway.configure()
            .dataSource(dataSource)
            .load();
        
        MigrationInfoService info = flyway.info();
        assertFalse(info.pending().length > 0, "存在未应用的迁移脚本");
    }
}

2. 异步代码测试

@Test
void shouldCompleteAsyncTask() {
    CompletableFuture<String> future = asyncService.processData();
    
    // 设置超时避免无限等待
    String result = future.get(5, TimeUnit.SECONDS);
    
    assertEquals("PROCESSED", result);
}

3. 安全上下文测试

@WebMvcTest(SecuredController.class)
@WithMockUser(roles = "ADMIN")
class SecuredControllerTest {
    
    @Test
    void shouldAllowAdminAccess() throws Exception {
        mockMvc.perform(get("/admin"))
            .andExpect(status().isOk());
    }
    
    @Test
    @WithMockUser(roles = "USER")
    void shouldDenyUserAccess() throws Exception {
        mockMvc.perform(get("/admin"))
            .andExpect(status().isForbidden());
    }
}

八、测试最佳实践

1. 测试命名规范

2. 测试数据管理

@BeforeEach
void setUp() {
    // 使用内存数据库准备数据
    testEntityManager.persist(new User("user1", "pass1"));
    testEntityManager.persist(new User("user2", "pass2"));
}

@AfterEach
void tearDown() {
    // 清理测试数据
    userRepository.deleteAll();
}

3. 测试执行优化

# 并行测试配置
junit.jupiter.execution.parallel.enabled = true
junit.jupiter.execution.parallel.mode.default = concurrent
junit.jupiter.execution.parallel.mode.classes.default = concurrent

九、测试报告与可视化

1. Allure测试报告

<dependency>
    <groupId>io.qameta.allure</groupId>
    <artifactId>allure-junit5</artifactId>
    <version>2.25.0</version>
</dependency>

2. 报告示例

十、完整测试套件示例

1. Maven测试配置

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-surefire-plugin</artifactId>
            <configuration>
                <includes>
                    <include>**/*Test.java</include>
                </includes>
                <excludes>
                    <exclude>**/*IntegrationTest.java</exclude>
                </excludes>
            </configuration>
        </plugin>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-failsafe-plugin</artifactId>
            <executions>
                <execution>
                    <goals>
                        <goal>integration-test</goal>
                        <goal>verify</goal>
                    </goals>
                    <configuration>
                        <includes>
                            <include>**/*IntegrationTest.java</include>
                        </includes>
                    </configuration>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

2. 测试目录结构

src/test
├── java
│   ├── unit
│   │   ├── service
│   │   ├── repository
│   │   └── util
│   └── integration
│       ├── repository
│       ├── api
│       └── external
└── resources
    ├── application-test.yml
    └── data.sql

总结:现代化测试体系价值

​实施路线图​​:

  1. ​阶段1​​:建立基础单元测试(JUnit 5 + Mockito)
  2. ​阶段2​​:集成数据库测试(Testcontainers)
  3. ​阶段3​​:添加Web层测试(@WebMvcTest)
  4. ​阶段4​​:构建完整CI/CD测试流水线