集成测试编排
本页提供 Testcontainers 完整示例(Java/Python/Node.js 启动 PostgreSQL)、事务回滚实现、supertest/httpx 集成测试、fixture 与 factory 数据管理,以及 GitHub Actions 并行运行配置。
集成测试应锁定真实协作边界(HTTP、gRPC、队列、ORM),在 SKILL 中声明启动顺序、健康检查与超时,避免「本地能跑、CI 偶发失败」。
数据准备优先可重复:事务回滚、每用例 schema 或命名空间隔离,并写明与生产配置的差异(如关闭外部 Webhook)。
对多服务仓库可要求 Agent 输出「依赖拓扑 + 最小可验证路径」,先覆盖主流程再扩展边缘集成场景。
- 用例间不共享可变全局状态,必要时使用唯一租户 ID 前缀。
- 异步消费需显式等待或轮询上限,并记录失败时的诊断日志要点。
- 与流水线矩阵对齐:指定镜像标签与资源配额,防止拉取漂移。
测试金字塔:集成层定位
集成层验证「多个真实组件拼在一起」仍满足契约:比单测慢、比 E2E 窄。SKILL 应写清:哪些边界必须走真实进程/端口(如 DB 协议、队列协议),哪些仍可用进程内替身,避免重复 E2E 已覆盖的浏览器路径。
┌─────────┐
╱ E2E ╱│ 关键旅程、少而全链路
╱────────╱ │
╱ 集成 ╱ │◀── 中层:真实 DB/队列/HTTP 邻域
╱────────╱ │ 断言协作契约与装配错误
╱ 单元 ╱ │
╱────────╱ │ 快:纯逻辑 + mock 进程外 I/O
────────── └── PR:单测为主,集成钉扎易碎边界
容器与本地编排
优先 Testcontainers、Docker Compose 或 CI 提供的等价服务:在 SKILL 中固定镜像 digest 或次版本标签,声明 depends_on 与健康检查后再跑迁移与测试,而不是裸 sleep。
- 网络:与生产一致的端口映射或服务名(compose 网络别名),避免硬编码
localhost与 CI 不一致。 - 数据卷:并行 job 使用独立 volume 或每次用例 TRUNCATE/事务,文档化清理成本。
- 资源:内存/CPU 上限与拉取重试写入流水线注释,便于 Agent 生成可复现的 workflow。
Testcontainers 完整示例:
Node.js(使用 testcontainers 包):
// tests/integration/setup.ts
import { PostgreSqlContainer } from '@testcontainers/postgresql';
import { Pool } from 'pg';
import { runMigrations } from '../../src/db/migrations';
let container: any;
let pool: Pool;
beforeAll(async () => {
// 启动 PostgreSQL 容器(固定版本,不用 latest)
container = await new PostgreSqlContainer('postgres:16.2-alpine')
.withDatabase('testdb')
.withUsername('testuser')
.withPassword('testpass')
.start();
pool = new Pool({
host: container.getHost(),
port: container.getMappedPort(5432),
database: container.getDatabase(),
user: container.getUsername(),
password: container.getPassword(),
});
// 运行数据库迁移
await runMigrations(pool);
}, 60_000); // 容器启动可能需要最长 60 秒
afterAll(async () => {
await pool.end();
await container.stop();
});
Python(使用 testcontainers-python):
# tests/conftest.py
import pytest
from testcontainers.postgres import PostgresContainer
from sqlalchemy import create_engine
from alembic.config import Config
from alembic import command
@pytest.fixture(scope="session")
def pg_container():
with PostgresContainer("postgres:16.2-alpine") as postgres:
engine = create_engine(postgres.get_connection_url())
# 运行 alembic 迁移
alembic_cfg = Config("alembic.ini")
alembic_cfg.set_main_option("sqlalchemy.url", postgres.get_connection_url())
command.upgrade(alembic_cfg, "head")
yield engine
每个测试后事务回滚(隔离测试数据,不需要每次清库):
// Node.js - 每测试事务回滚
let client: any;
beforeEach(async () => {
client = await pool.connect();
await client.query('BEGIN'); // 开始事务
});
afterEach(async () => {
await client.query('ROLLBACK'); // 回滚所有写操作
client.release();
});
it('should create order and reduce inventory', async () => {
// 这里的 INSERT/UPDATE 操作都会在 afterEach 中 ROLLBACK
await client.query('INSERT INTO orders (user_id, total) VALUES ($1, $2)', [1, 99.50]);
const result = await client.query('SELECT COUNT(*) FROM orders WHERE user_id = $1', [1]);
expect(parseInt(result.rows[0].count)).toBe(1);
// afterEach ROLLBACK 后,此数据不会留存影响其他测试
});
supertest 集成测试(含认证 header 和完整断言):
// tests/integration/orders.test.ts
import request from 'supertest';
import { app } from '../../src/app';
import { generateTestToken } from '../helpers/auth';
describe('POST /api/orders', () => {
const authToken = generateTestToken({ userId: 'test-user-1', role: 'customer' });
it('should create order with valid payload', async () => {
const response = await request(app)
.post('/api/orders')
.set('Authorization', `Bearer ${authToken}`)
.set('Content-Type', 'application/json')
.send({
items: [{ productId: 'prod-1', quantity: 2 }],
couponCode: 'SAVE10'
});
expect(response.status).toBe(201);
expect(response.body).toMatchObject({
orderId: expect.stringMatching(/^ORD-\d+/),
total: expect.any(Number),
status: 'pending'
});
expect(response.headers['location']).toMatch(/\/api\/orders\/ORD-/);
});
it('should return 401 without auth header', async () => {
const response = await request(app).post('/api/orders').send({});
expect(response.status).toBe(401);
expect(response.body.error).toBe('UNAUTHORIZED');
});
});
测试数据管理:fixture 文件格式 + factory 函数:
// tests/fixtures/orders.json
{
"validOrder": {
"userId": "user-001",
"items": [{ "productId": "prod-001", "quantity": 2, "price": 49.75 }],
"total": 99.50,
"status": "pending"
},
"expiredCoupon": {
"code": "OLD10",
"discount": 10,
"expiresAt": "2020-01-01T00:00:00Z"
}
}
// tests/factories/orderFactory.ts
import { v4 as uuidv4 } from 'uuid';
export function createOrder(overrides = {}) {
return {
orderId: `ORD-${uuidv4().slice(0, 8)}`,
userId: 'test-user-1',
items: [{ productId: 'prod-001', quantity: 1, price: 99.00 }],
total: 99.00,
status: 'pending',
createdAt: new Date().toISOString(),
...overrides // 允许覆盖任意字段
};
}
// 使用示例:
const orderWithCoupon = createOrder({ couponCode: 'SAVE10', total: 89.10 });
const cancelledOrder = createOrder({ status: 'cancelled' });
GitHub Actions 中并行运行集成测试:
name: Integration Tests
on: [push, pull_request]
jobs:
integration-test:
runs-on: ubuntu-latest
strategy:
matrix:
# 按模块分片并行运行(4 个 worker)
suite: [orders, payments, inventory, auth]
fail-fast: false # 一个失败不停止其他
services:
postgres:
image: postgres:16.2-alpine
env:
POSTGRES_DB: testdb
POSTGRES_USER: testuser
POSTGRES_PASSWORD: testpass
options: >-
--health-cmd pg_isready
--health-interval 5s
--health-timeout 5s
--health-retries 10
ports:
- 5432:5432
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- run: npm run db:migrate
env:
DATABASE_URL: postgresql://testuser:testpass@localhost:5432/testdb
- run: npm run test:integration -- --testPathPattern=${{ matrix.suite }}
env:
DATABASE_URL: postgresql://testuser:testpass@localhost:5432/testdb
- uses: actions/upload-artifact@v4
if: failure()
with:
name: integration-test-results-${{ matrix.suite }}
path: test-results/
用例生命周期(编排顺序)
在 SKILL 中把一次集成跑拆成可观测阶段,方便失败时定位是「起不来」还是「数据/断言」问题。
[ 解析环境:镜像标签、连接串、feature 开关 ]
│
▼
┌─────────────┐ 并行矩阵:按套件分片时注明共享资源冲突
│ 拉起依赖容器 │──── healthcheck / wait-for-it,超时写进日志
└─────────────┘
│
▼
┌─────────────┐ 顺序:schema 迁移 → 种子数据(可选)→ 被测进程
│ 迁移与夹具 │──── 每用例:事务、独立租户前缀或命名库
└─────────────┘
│
▼
┌─────────────┐ HTTP/gRPC/队列:同步点 + 异步轮询上限
│ 执行断言 │──── 失败:保留容器日志、topic lag、SQL 状态
└─────────────┘
│
▼
[ teardown:停容器 / 删 volume / 上报 junit ]
小工具:服务依赖清单
勾选本项目集成测试实际涉及的外部依赖,生成可粘贴进 SKILL 或 PR 说明的 Markdown 片段;状态会保存在本机浏览器。
---
name: integration-testing-cn
description: Testcontainers 编排、事务隔离、supertest 断言与 CI 并行策略
---
# 步骤 1:启动测试容器(Testcontainers)
Node.js:
import { PostgreSqlContainer } from '@testcontainers/postgresql';
const container = await new PostgreSqlContainer('postgres:16.2-alpine').start();
Python:
from testcontainers.postgres import PostgresContainer
with PostgresContainer("postgres:16.2-alpine") as pg:
engine = create_engine(pg.get_connection_url())
# 步骤 2:运行数据库迁移
Node: await runMigrations(pool) # 在 beforeAll 中,容器启动后执行
Python: alembic upgrade head
# 步骤 3:每测试事务回滚(隔离数据)
beforeEach: await client.query('BEGIN')
afterEach: await client.query('ROLLBACK')
# 不需要 TRUNCATE,每个测试的写操作都会 ROLLBACK
# 步骤 4:supertest 集成测试结构
import request from 'supertest';
const res = await request(app)
.post('/api/orders')
.set('Authorization', `Bearer ${token}`)
.send(payload);
expect(res.status).toBe(201);
expect(res.body).toMatchObject({ orderId: expect.stringMatching(/ORD-/) });
# 步骤 5:测试数据管理
fixture 文件: tests/fixtures/*.json(静态典型数据)
factory 函数: tests/factories/*.ts(动态生成含 uuid)
原则: factory 覆盖 → 只传与测试相关的字段,其余用默认值
# 步骤 6:CI 并行策略
matrix.suite: [orders, payments, inventory, auth] # 按模块分片
fail-fast: false # 一个失败不影响其他
services.postgres: healthcheck pg_isready --health-retries 10
artifact: 失败时上传 test-results/ 目录