示例说明
内存数据库与真库的行为差异是集成测试最大的谎言来源:约束、锁、类型系统都可能不同。Testcontainers 从 Docker 拉起与生产同版本的 PostgreSQL,测试跑在真实引擎上;容器生命周期由测试框架托管,就绪探测内建——start 返回时数据库已可接受连接,套件结束 stop 销毁不留残余。
技术要点
- 镜像版本与生产环境对齐
- fsync 关闭加速测试型写入
- beforeAll 单容器共享降低开销
代码解读
容器配置指定数据库名与凭据并关闭 fsync 提升测试吞吐;start 返回连接串后建发票表并带 CHECK 约束。用例执行真实的参数化插入与回读,计数断言验证状态落地。afterAll 先还连接池再停容器,顺序错误会报连接中止。
使用建议
容器启动有秒级成本,套件级复用而非用例级。CI 运行器需要 Docker 支持。密码凭据仅测试用,不要复用到任何真实环境。
代码示例
import { PostgreSqlContainer } from '@testcontainers/postgresql';
import { Pool } from 'pg';
import { afterAll, beforeAll, test } from 'vitest';
let container;
let pool: Pool;
beforeAll(async () => {
container = await new PostgreSqlContainer('postgres:16-alpine')
.withDatabase('app_test')
.withUsername('tester')
.withPassword('secret')
.withCommand(['postgres', '-c', 'fsync=off'])
.start();
pool = new Pool({ connectionString: container.getConnectionUri() });
await pool.query(`
CREATE TABLE invoices (
id SERIAL PRIMARY KEY,
total_cents INTEGER NOT NULL CHECK (total_cents >= 0)
)
`);
}, 120_000);
test('insert and read back within a real postgres engine', async () => {
const inserted = await pool.query(
'INSERT INTO invoices (total_cents) VALUES ($1) RETURNING id, total_cents',
[1999],
);
expect(inserted.rows[0].total_cents).toBe(1999);
const read = await pool.query('SELECT count(*)::int AS n FROM invoices');
expect(read.rows[0].n).toBe(1);
});
afterAll(async () => {
await pool?.end();
await container?.stop();
});