← 中文资源

简体中文版本

用 Testcontainers 运行集成测试

每次测试启动一次性 PostgreSQL 容器,执行真实迁移并在结束后销毁,让测试不再共用同一个数据库。

阅读英文原文

概述

共享的测试数据库会不断腐化:半成品数据、被改动的配置、某个分支执行过而另一个分支没执行的迁移。测试因为与代码无关的原因失败,重跑又通过,团队逐渐学会忽略红色构建。一次性容器彻底消除了共享可变状态:每次运行都从同一个镜像和同一批迁移开始。

步骤

  1. 安装依赖:
pnpm add -D testcontainers @testcontainers/postgresql pg
  1. 在 before 钩子里启动容器、建立连接池并执行迁移:
container = await new PostgreSqlContainer('postgres:16-alpine')
  .withDatabase('app_test')
  .start();
pool = new pg.Pool({ connectionString: container.getConnectionUri() });
await runMigrations(pool);
  1. 在 after 钩子里先 pool.end() 再 container.stop(),避免连接与容器泄漏到下一次运行。
  2. 编写只有真实引擎才能验证的断言,例如重复插入触发 23505、部分唯一索引拒绝活跃重复行、可序列化事务返回 40001。

验证

pnpm test:integration 在干净的机器上应全部通过;首次运行会拉取镜像,之后启动通常在 5 秒内完成。故意删除一个唯一约束后,重复插入的断言应失败,说明测试确实覆盖了 schema 契约。运行结束后 docker ps 中不应残留测试容器。

注意事项

  • 必须能访问 Docker 守护进程;没有容器运行时的环境应显式跳过集成测试而不是伪装通过。
  • 每个测试文件或 worker 启动一个容器即可,不要为每个用例启动,否则启动开销会占满运行时间。
  • 不要映射固定端口,使用 getConnectionUri() 或 getMappedPort() 让 Docker 分配空闲端口。
  • 固定镜像标签(如 postgres:16-alpine)而不是 latest,避免次版本更新悄悄改变行为。
  • 容器是真实 PostgreSQL,但不等同于生产:扩展、参数调优与版本仍可能不同,容量类问题需要额外的性能测试。