SpringBootTest + Mockito + Testcontainers 深度集成指南

SpringBootTest + Mockito + Testcontainers 深度集成指南

本文将详细讲解如何将 SpringBootTest、Mockito 和 Testcontainers 三者结合,构建现代化的分层测试策略,提供从单元测试到集成测试的完整解决方案。

一、架构设计与核心优势

SpringBootTest
应用上下文管理
自动配置
Mockito
依赖模拟
行为验证
Testcontainers
真实服务容器
环境隔离
集成优势
分层测试
真实集成
部分模拟

核心价值

  • 分层测试:单元测试 → 集成测试 → 端到端测试
  • 环境真实性:容器化依赖 = 生产环境
  • 效率平衡:关键路径真实测试 + 非关键路径模拟
  • 资源优化:按需启动容器,复用应用上下文

二、Maven 依赖配置

<dependencies>
    <!-- Spring Boot Starter Test -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <version>3.0.4</version>
        <scope>test</scope>
    </dependency>
    
    <!-- Testcontainers -->
    <dependency>
        <groupId>org.testcontainers</groupId>
        <artifactId>testcontainers</artifactId>
        <version>1.17.6</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.testcontainers</groupId>
        <artifactId>junit-jupiter</artifactId>
        <version>1.17.6</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.testcontainers</groupId>
        <artifactId>postgresql</artifactId>
        <version>1.17.6</version>
        <scope>test</scope>
    </dependency>
    
    <!-- Database -->
    <dependency>
        <groupId>org.postgresql</groupId>
        <artifactId>postgresql</artifactId>
        <scope>runtime</scope>
    </dependency>
</dependencies>

三、基础项目结构

领域模型

@Entity
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String username;
    private String email;
    private boolean active;
    
    // getters/setters
}

仓库接口

public interface UserRepository extends JpaRepository<User, Long> {
    List<User> findByActive(boolean active);
}

服务层

@Service
@RequiredArgsConstructor
public class UserService {
    private final UserRepository userRepository;
    private final EmailService emailService;
    
    public User registerUser(String username, String email) {
        User user = new User();
        user.setUsername(username);
        user.setEmail(email);
        user.setActive(false);
        
        User savedUser = userRepository.save(user);
        emailService.sendActivationEmail(savedUser.getEmail());
        return savedUser;
    }
    
    public User activateUser(Long userId) {
        User user = userRepository.findById(userId)
            .orElseThrow(() -> new UserNotFoundException("User not found"));
        
        user.setActive(true);
        return userRepository.save(user);
    }
}

邮件服务

@Service
public class EmailService {
    public void sendActivationEmail(String email) {
        // 实际邮件发送逻辑
    }
}

四、分层测试策略

1. 单元测试层(Mockito)

@ExtendWith(MockitoExtension.class)
class UserServiceUnitTest {

    @Mock
    private UserRepository userRepository;
    
    @Mock
    private EmailService emailService;
    
    @InjectMocks
    private UserService userService;
    
    @Test
    void registerUser_ValidInput_CreatesUser() {
        // given
        String username = "testuser";
        String email = "test@example.com";
        User savedUser = new User(1L, username, email, false);
        
        when(userRepository.save(any(User.class))).thenReturn(savedUser);
        
        // when
        User result = userService.registerUser(username, email);
        
        // then
        assertThat(result.getId()).isEqualTo(1L);
        verify(emailService).sendActivationEmail(email);
    }
}

2. 集成测试层(SpringBootTest + Testcontainers)

@SpringBootTest
@Testcontainers
@ActiveProfiles("test")
class UserServiceIntegrationTest {

    @Container
    static final PostgreSQLContainer<?> POSTGRES = 
        new PostgreSQLContainer<>("postgres:15-alpine")
            .withDatabaseName("testdb")
            .withUsername("test")
            .withPassword("test");
    
    @Autowired
    private UserRepository userRepository;
    
    @MockBean
    private EmailService emailService;
    
    @Autowired
    private UserService userService;
    
    @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);
    }
    
    @Test
    void activateUser_ShouldUpdateDatabase() {
        // given
        User user = new User();
        user.setUsername("test");
        user.setEmail("test@example.com");
        user.setActive(false);
        User savedUser = userRepository.save(user);
        
        // when
        User activatedUser = userService.activateUser(savedUser.getId());
        
        // then
        assertThat(activatedUser.isActive()).isTrue();
        
        // 验证数据库状态
        User dbUser = userRepository.findById(savedUser.getId()).get();
        assertThat(dbUser.isActive()).isTrue();
        
        // 验证模拟服务调用
        verify(emailService, never()).sendActivationEmail(anyString());
    }
}

3. 端到端测试层(完整容器化)

@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
@Testcontainers
@ActiveProfiles("e2e")
class UserControllerE2ETest {

    @Container
    static final PostgreSQLContainer<?> POSTGRES = 
        new PostgreSQLContainer<>("postgres:15-alpine")
            .withDatabaseName("e2edb")
            .withUsername("e2e")
            .withPassword("e2e");
    
    @Container
    static final GenericContainer<?> SMTP = new GenericContainer<>("mailhog/mailhog")
        .withExposedPorts(1025, 8025);
    
    @Autowired
    private TestRestTemplate restTemplate;
    
    @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);
        
        // 邮件服务配置
        registry.add("spring.mail.host", SMTP::getHost);
        registry.add("spring.mail.port", SMTP::getFirstMappedPort);
    }
    
    @Test
    void registerUser_ShouldSendEmail() {
        // given
        Map<String, String> request = Map.of(
            "username", "e2euser",
            "email", "e2e@example.com"
        );
        
        // when
        ResponseEntity<Void> response = restTemplate.postForEntity(
            "/api/users/register", 
            request, 
            Void.class
        );
        
        // then
        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED);
        
        // 验证邮件发送
        String mailhogApi = "http://%s:%s/api/v2/messages".formatted(
            SMTP.getHost(), 
            SMTP.getMappedPort(8025)
        );
        
        ResponseEntity<String> emails = restTemplate.getForEntity(
            mailhogApi, String.class);
        
        assertThat(emails.getBody()).contains("e2e@example.com");
    }
}

五、高级集成技巧

1. 容器复用策略

public abstract class BaseIntegrationTest {

    static final PostgreSQLContainer<?> POSTGRES;
    
    static {
        POSTGRES = new PostgreSQLContainer<>("postgres:15-alpine")
            .withDatabaseName("testdb")
            .withReuse(true);
        POSTGRES.start();
    }
    
    @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);
    }
}

@SpringBootTest
class UserServiceTest extends BaseIntegrationTest {
    // 测试方法
}

2. 自定义测试切片

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@SpringBootTest
@Testcontainers
@ActiveProfiles("test")
@AutoConfigureMockMvc
@Import(TestConfig.class)
public @interface ContainerizedTest {
}

@ContainerizedTest
class UserControllerTest {
    
    @Container
    static PostgreSQLContainer<?> postgres = //...
    
    @Autowired
    private MockMvc mockMvc;
    
    // 测试方法
}

3. 混合模拟策略

@SpringBootTest
@Testcontainers
class MixedMockTest {

    @Container
    static PostgreSQLContainer<?> postgres = //...
    
    // 真实数据库仓库
    @Autowired
    private UserRepository userRepository;
    
    // 模拟邮件服务
    @MockBean
    private EmailService emailService;
    
    // 模拟第三方支付服务
    @MockBean
    private PaymentGateway paymentGateway;
    
    @Autowired
    private OrderService orderService;
    
    @Test
    void createOrder_ShouldUseRealDbAndMockExternal() {
        // 使用真实数据库
        User user = userRepository.save(new User("testuser", "test@example.com"));
        
        // 配置模拟服务
        when(paymentGateway.process(any())).thenReturn(true);
        
        // 执行测试
        Order order = orderService.createOrder(user.getId(), /* ... */);
        
        // 验证混合交互
        assertThat(order).isNotNull();
        verify(paymentGateway).process(any());
        verify(emailService).sendOrderConfirmation(anyString());
    }
}

六、性能优化方案

1. 应用上下文缓存

@SpringBootTest
@ContextConfiguration(initializers = TestContainersInitializer.class)
@Testcontainers
@DirtiesContext(classMode = ClassMode.AFTER_CLASS)
class CachedContextTest {
    // 测试类共享相同上下文
}

class TestContainersInitializer 
    implements ApplicationContextInitializer<ConfigurableApplicationContext> {
    
    static final PostgreSQLContainer<?> POSTGRES = 
        new PostgreSQLContainer<>("postgres:15-alpine")
            .withReuse(true);
    
    static {
        POSTGRES.start();
    }
    
    @Override
    public void initialize(ConfigurableApplicationContext context) {
        TestPropertyValues.of(
            "spring.datasource.url=" + POSTGRES.getJdbcUrl(),
            "spring.datasource.username=" + POSTGRES.getUsername(),
            "spring.datasource.password=" + POSTGRES.getPassword()
        ).applyTo(context.getEnvironment());
    }
}

2. 容器启动优化

public class FastStartPostgreSQLContainer extends PostgreSQLContainer<FastStartPostgreSQLContainer> {

    private static final String IMAGE = "postgres:15-alpine";
    private static FastStartPostgreSQLContainer container;
    
    private FastStartPostgreSQLContainer() {
        super(IMAGE);
    }
    
    public static FastStartPostgreSQLContainer getInstance() {
        if (container == null) {
            container = new FastStartPostgreSQLContainer()
                .withDatabaseName("test")
                .withUsername("test")
                .withPassword("test");
            container.start();
        }
        return container;
    }
    
    @Override
    public void start() {
        // 避免重复启动
        if (!isRunning()) {
            super.start();
        }
    }
    
    @Override
    public void stop() {
        // 测试结束后不停止容器
    }
}

七、CI/CD 集成示例

GitHub Actions 配置

name: CI with Testcontainers

on: [push]

jobs:
  build:
    runs-on: ubuntu-latest
    services:
      docker:
        image: docker:dind
        options: --privileged
        
    steps:
    - name: Checkout
      uses: actions/checkout@v3
      
    - name: Set up JDK 17
      uses: actions/setup-java@v3
      with:
        java-version: '17'
        distribution: 'temurin'
        
    - name: Build with Maven
      run: mvn verify
      env:
        TESTCONTAINERS_RYUK_DISABLED: true
        DOCKER_HOST: tcp://localhost:2375

八、最佳实践总结

1. 分层测试策略

2. 配置管理原则

配置类型推荐方案
数据库连接Testcontainers 动态注入
外部服务端点容器网络别名 + 服务发现
敏感信息Testcontainers 内置安全机制
性能相关单独配置文件(test-profile)

3. 测试数据管理

@Test
void testWithData(@Autowired TestEntityManager em) {
    // 使用TestEntityManager准备数据
    User user = em.persistFlushFind(new User("test", "test@example.com"));
    
    // 执行测试...
    
    // 验证后清理
    em.remove(user);
}

4. 测试分类执行

# src/test/resources/application-test.properties
spring.test.context.cache.maxSize=5
spring.jpa.show-sql=true

# JUnit 5 标签配置
junit.jupiter.execution.parallel.enabled=true
junit.jupiter.execution.parallel.config.strategy=dynamic
@Tag("integration")
@ContainerizedTest
class IntegrationTest { /* ... */ }

@Tag("e2e")
@SpringBootTest(webEnvironment = RANDOM_PORT)
class E2ETest { /* ... */ }

九、常见问题解决方案

问题解决方案
容器启动超时增加等待策略 waitingFor(Wait.forHealthcheck())
端口冲突使用动态端口映射 withExposedPorts(0)
Spring上下文刷新慢使用 @DirtiesContext 控制刷新范围
测试间数据污染使用 @Transactional 自动回滚
Windows/Mac文件挂载问题使用类路径资源映射替代文件绑定
CI环境中Docker不可用配置Docker-in-Docker(DinD)

核心价值总结

  1. 环境一致性:容器化依赖确保测试/生产环境一致
  2. 测试可信度:关键路径真实集成提升测试可靠性
  3. 资源效率:按需启动容器,复用应用上下文
  4. 灵活组合:自由选择真实服务与模拟服务
  5. 现代化栈:基于JUnit5的统一测试平台

专家建议

  • 核心业务流使用完整容器化测试
  • 非关键路径服务使用Mockito模拟
  • 数据库操作必须使用真实数据库测试
  • 在CI中启用容器复用加速测试
  • 监控测试执行时间,优化慢速测试

完整项目测试结构:

src/test/java/
├── unit/
│   ├── UserServiceUnitTest.java
│   └── EmailServiceUnitTest.java
├── integration/
│   ├── UserServiceIntegrationTest.java
│   └── PaymentIntegrationTest.java
├── e2e/
│   ├── UserControllerE2ETest.java
│   └── OrderControllerE2ETest.java
└── base/
    ├── BaseIntegrationTest.java
    └── TestContainersInitializer.java
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值