概述
共享的测试数据库会不断腐化:半成品数据、被改动的配置、某个分支执行过而另一个分支没执行的迁移。测试因为与代码无关的原因失败,重跑又通过,团队逐渐学会忽略红色构建。一次性容器彻底消除了共享可变状态:每次运行都从同一个镜像和同一批迁移开始。
步骤
- 安装依赖:
pnpm add -D testcontainers @testcontainers/postgresql pg
- 在
before钩子里启动容器、建立连接池并执行迁移:
container = await new PostgreSqlContainer('postgres:16-alpine')
.withDatabase('app_test')
.start();
pool = new pg.Pool({ connectionString: container.getConnectionUri() });
await runMigrations(pool);
- 在
after钩子里先pool.end()再container.stop(),避免连接与容器泄漏到下一次运行。 - 编写只有真实引擎才能验证的断言,例如重复插入触发
23505、部分唯一索引拒绝活跃重复行、可序列化事务返回40001。
验证
pnpm test:integration 在干净的机器上应全部通过;首次运行会拉取镜像,之后启动通常在 5 秒内完成。故意删除一个唯一约束后,重复插入的断言应失败,说明测试确实覆盖了 schema 契约。运行结束后 docker ps 中不应残留测试容器。
注意事项
- 必须能访问 Docker 守护进程;没有容器运行时的环境应显式跳过集成测试而不是伪装通过。
- 每个测试文件或 worker 启动一个容器即可,不要为每个用例启动,否则启动开销会占满运行时间。
- 不要映射固定端口,使用
getConnectionUri()或getMappedPort()让 Docker 分配空闲端口。 - 固定镜像标签(如
postgres:16-alpine)而不是latest,避免次版本更新悄悄改变行为。 - 容器是真实 PostgreSQL,但不等同于生产:扩展、参数调优与版本仍可能不同,容量类问题需要额外的性能测试。