单元测试可以用 Mock 隔离依赖,但只要进入数据库、缓存这类集成测试场景,测试结果是否可信,很大程度上取决于你有没有真实运行环境。Testcontainers 的价值就在这里:测试启动时临时拉起 Docker 容器,跑完自动销毁,既避免手工维护测试库,也能减少环境污染。
下面按 Spring Boot 项目的常见落地顺序来拆解:先补齐依赖,再分别看 MySQL 和 Redis 的接入方式,最后再处理多种中间件并存时的启动成本问题。读完你可以直接判断,项目里该如何组织测试类、动态注入连接信息,以及什么时候适合把容器做成共享基类。
先把 Testcontainers 依赖补齐
先用 Maven 引入 Testcontainers 核心包和 MySQL 模块:
org.testcontainers
testcontainers
1.19.3
test
org.testcontainers
mysql
1.19.3
test
这里的版本统一使用 1.19.3。实际项目中可以升级到新的稳定版,但核心原则不变:相关模块的版本号尽量保持一致,避免测试依赖之间出现兼容性问题。
MySQL 集成测试怎么接入
数据库是最常见的集成测试场景。做法并不复杂:在测试类上标记 @Testcontainers,再声明一个静态 MySQLContainer,让 Spring 在测试启动时拿到容器生成的连接信息。

测试类里声明 MySQL 容器
@SpringBootTest
@Testcontainers
class UserRepositoryTest {
@Container
static MySQLContainer> mysql = new MySQLContainer<>("mysql:8.0")
.withDatabaseName("testdb")
.withUsername("test")
.withPassword("test");
@DynamicPropertySource
static void configureProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", mysql::getJdbcUrl);
registry.add("spring.datasource.username", mysql::getUsername);
registry.add("spring.datasource.password", mysql::getPassword);
}
@Autowired
private UserRepository userRepository;
@Test
void testSaveAndFind() {
User user = new User();
user.setName("张三");
user.setEmail("zhangsan@test.com");
userRepository.save(user);
assertThat(user.getId()).isNotNull();
User found = userRepository.findById(user.getId()).orElse(null);
assertThat(found).isNotNull();
assertThat(found.getName()).isEqualTo("张三");
}
}为什么关键点是 @DynamicPropertySource
@DynamicPropertySource 是这类测试能跑通的关键。因为容器每次启动后,JDBC 地址、用户名、密码都可能变化,不能提前写死在配置文件里。通过这个注解,Spring 会在测试上下文初始化时,把容器实际生成的连接参数动态注入到:
spring.datasource.urlspring.datasource.usernamespring.datasource.password
这样测试方法里就可以直接按正常仓储逻辑做保存、查询和断言,不需要关心数据库部署在哪里,也不用预先维护一套固定测试库。
Redis 集成测试怎么写
缓存测试的思路和数据库一致,也是先启动真实容器,再把运行时连接信息交给 Spring。这里额外补一个 Redis 依赖:
org.testcontainers
redis
1.19.3
test
用 GenericContainer 启动 Redis 7
@SpringBootTest
@Testcontainers
class RedisCacheTest {
@Container
static GenericContainer> redis = new GenericContainer<>(
DockerImageName.parse("redis:7-alpine"))
.withExposedPorts(6379);
@DynamicPropertySource
static void configureProperties(DynamicPropertyRegistry registry) {
registry.add("spring.redis.host", redis::getHost);
registry.add("spring.redis.port", () -> redis.getMappedPort(6379));
}
@Autowired
private StringRedisTemplate redisTemplate;
@Test
void testSetAndGet() {
redisTemplate.opsForValue().set("test:key", "hello");
String value = redisTemplate.opsForValue().get("test:key");
assertThat(value).isEqualTo("hello");
}
}这里要特别注意 host 和 port 映射
Redis 示例里使用的是 GenericContainer,镜像为 redis:7-alpine,并暴露 6379 端口。和数据库测试不同,这里需要分别把:
spring.redis.host绑定到redis.getHost()spring.redis.port绑定到redis.getMappedPort(6379)
原因是 Docker 暴露出来的宿主机端口通常不是固定值,直接写死 6379 往往会导致测试连不上。动态读取映射端口,才是稳定做法。完成这一步后,测试里就可以直接使用 StringRedisTemplate 进行读写校验,行为和真实运行环境基本一致。
MySQL 和 Redis 一起测时,怎么减少启动开销
如果项目中既测数据库,又测缓存,每个测试类都单独启动一次容器,整体耗时会比较明显。这种情况下,更实用的做法是抽出一个共享基类,把容器定义成静态成员,让多个测试类复用。

抽一个共享的抽象基类
@Testcontainers
abstract class AbstractIntegrationTest {
@Container
static MySQLContainer> mysql = new MySQLContainer<>("mysql:8.0");
@Container
static GenericContainer> redis = new GenericContainer<>(
DockerImageName.parse("redis:7-alpine"))
.withExposedPorts(6379);
@DynamicPropertySource
static void configureProperties(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.redis.host", redis::getHost);
registry.add("spring.redis.port", () -> redis.getMappedPort(6379));
}
}共享容器适合什么场景
这种组织方式的收益主要有两点:
- 整个测试套件中,MySQL 和 Redis 容器只需要启动一次,能明显缩短执行时间。
- 所有子类测试都拿到同一套动态注入配置,避免重复编写容器初始化代码。
同时,Testcontainers 在测试结束后仍会自动销毁容器,因此即使做了复用,也不会留下长期运行的脏环境。对于需要频繁跑集成测试的 Spring Boot 项目来说,这通常比手工维护测试库、测试缓存更省事,也更接近“真实依赖、临时环境、执行后清理”的理想状态。







