← 中文资源

简体中文版本

Testcontainers PostgreSQL 集成测试

用 Testcontainers 为集成测试拉起一次性 PostgreSQL 容器:就绪探测后建表跑真查询,套件结束自动销毁,数据库规格与生产一致。

阅读英文原文

示例说明

内存数据库与真库的行为差异是集成测试最大的谎言来源:约束、锁、类型系统都可能不同。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();
});