集成测试编排

本页提供 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/ 目录

返回技能库 更多技能入口