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) |
核心价值总结:
- 环境一致性:容器化依赖确保测试/生产环境一致
- 测试可信度:关键路径真实集成提升测试可靠性
- 资源效率:按需启动容器,复用应用上下文
- 灵活组合:自由选择真实服务与模拟服务
- 现代化栈:基于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