在非Spring框架的应用里做数据库集成测试,最大的麻烦往往不是写SQL,而是怎么让测试代码自己决定连哪个库、用哪套账号。Testcontainers提供了一组轻量API,可以在测试运行前自动启动Docker里的真实数据库,再把动态分配的连接信息交给你的DAO层。这种方式不依赖Spring的自动装配,也不需要在项目里放一份写死的测试库配置。
为什么非Spring应用更需要动态配置
很多使用Vert.x、Dropwizard或者干脆是main函数启动的Java程序,数据库地址通常写在properties文件或环境变量中。如果为了集成测试专门改配置,既容易漏改,也会让本地和CI环境不一致。Testcontainers的思路是:测试代码自己当老板,它启动容器,拿到实际映射端口,然后直接把这些值塞进你的连接池构造参数里。
和Spring生态里那种靠注解注入不同,非Spring应用要显式写几行代码来管理容器生命周期。好处是逻辑完全透明,你清楚知道数据库什么时候起、什么时候停,也方便在多个测试类之间共享同一个容器实例,节省重复拉起的时间。
使用GenericContainer启动PostgreSQL
最通用的做法是直接用GenericContainer类。下面这段Java代码演示了在JUnit 5里启动一个PostgreSQL,并动态读出JDBC URL:
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.utility.DockerImageName;
import org.junit.jupiter.api.Test;
public class UserRepoTest {
// 启动容器,映射默认5432到随机主机端口
private static final GenericContainer<?> pg = new GenericContainer<>(
DockerImageName.parse("postgres:15-alpine"))
.withExposedPorts(5432)
.withEnv("POSTGRES_PASSWORD", "test")
.withEnv("POSTGRES_USER", "test")
.withEnv("POSTGRES_DB", "app_test");
@Test
void shouldInsertAndQueryUser() {
// 容器必须在用例前启动,这里简化为静态块或BeforeAll
pg.start();
String host = pg.getHost();
Integer port = pg.getMappedPort(5432);
String jdbcUrl = "jdbc:postgresql://" + host + ":" + port + "/app_test";
// 把jdbcUrl、test、test传给你的DataSource工厂
System.out.println("动态连接串:" + jdbcUrl);
// 执行业务断言...
pg.stop();
}
}
上面代码里,getHost和getMappedPort就是动态配置的核心。它们返回的不再是localhost加固定端口,而是Docker引擎分配的真实可达地址。你的非Spring应用可以用这个字符串去创建HikariCP或者直接用DriverManager连接。
需要注意,GenericContainer不会自动帮你生成JDBC URL格式,所以像上面那样手拼字符串是必须的。如果拼错,测试就会连到空地方。另外容器停止最好放在AfterAll里,而不是每个方法都启停,否则测试会非常慢。
更简单的JDBC专用模块
Testcontainers提供了PostgreSQLContainer等专用子类,封装了常用环境变量和URL拼接。对于非Spring应用,这能少写一点样板代码:
import org.testcontainers.containers.PostgreSQLContainer;
import org.junit.jupiter.api.Test;
public class OrderTest {
private static final PostgreSQLContainer<?> db = new PostgreSQLContainer<>("postgres:15-alpine");
@Test
void checkOrderTable() {
db.start();
// 专用类直接给出标准JDBC URL
String url = db.getJdbcUrl();
String user = db.getUsername();
String pwd = db.getPassword();
System.out.println(url + " | " + user + " | " + pwd);
// 传入自定义连接管理器
db.stop();
}
}
从代码可以看出,PostgreSQLContainer已经内置了getJdbcUrl方法,内部处理了数据库名与驱动协议。非Spring项目只要调用这几个getter,就能完成动态配置,不需要自己维护镜像参数文档。
这种方式的缺点是绑定了具体数据库类型,如果你测的是MySQL就得换MySQLContainer。但换来的是更少的错误率和更清晰的语义,对中小团队很划算。
在测试基类中复用容器
非Spring应用通常没有依赖注入容器帮我们做单例,所以要靠静态变量加JUnit生命周期来共享。下面是一个基类写法:
import org.testcontainers.containers.PostgreSQLContainer;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.AfterAll;
public abstract class BaseDbTest {
protected static PostgreSQLContainer<?> sharedDb;
@BeforeAll
static void initDb() {
sharedDb = new PostgreSQLContainer<>("postgres:15-alpine");
sharedDb.start();
}
@AfterAll
static void closeDb() {
if (sharedDb != null) {
sharedDb.stop();
}
}
}
业务测试类继承BaseDbTest后,直接读sharedDb.getJdbcUrl()即可。所有子类共用一个容器,既快又省资源。但要注意静态顺序,别让某个子类误调用stop导致别人断连。
如果应用本身有配置加载器,可以在基类里用反射或系统属性把动态URL注入进去,这样业务代码完全无感知,就像读普通配置文件一样。
与硬编码配置方案的对比
我们把三种常见做法放一起看看:
| 方案 | 环境一致性 | 代码改动量 | 适用非Spring |
|---|---|---|---|
| 写死测试库地址 | 低,易冲突 | 少 | 直接可用 |
| 手动开Docker再连 | 中,靠人维护 | 中 | 可用 |
| Testcontainers动态 | 高,随起随销 | 稍多但一次性的 | 非常合适 |
从表里能看出,动态配置虽然要多写一点容器代码,但它把环境不确定性压到了最低。非Spring应用没有框架兜底,反而更该用这种显式可控的方式。
另外,Testcontainers支持在CI里复用Docker守护进程,只要机器装了Docker,测试就能跑,不需要额外数据库服务,这对非Spring的独立服务尤其友好。
避坑与小结
一个常见误区是以为Testcontainers只能配合Spring Test注解。其实它的核心就是普通Java对象,任何main或JUnit环境都能用。另一个坑是忘记调用start,或者把容器建在测试方法内部导致端口每次都变,拖慢整体构建。
总结来说,非Spring应用通过GenericContainer或数据库专用Container,在测试前拿到动态连接信息,再传给自己的连接层,就能用真实数据库做集成测试。整个过程不碰Spring,不写死配置,干净且贴近生产。
Testcontainers集成测试数据库连接修改时间:2026-08-01 13:09:36