--- name: qa/integration-testing description: 集成测试方法论,验证模块间协作、数据库交互、服务间调用,参考 Testcontainers、Testing Library、WireMock 等开源实践 version: 1.0.0 agent: qa type: methodology user-invocable: false agent-invocable: true dependencies: - shared/tech-stack-detection triggers: - 需要验证模块间协作时 - 需要测试数据库真实交互时 - 需要测试服务间调用时 - 需要验证前端组件与 API 集成时 metadata: internal: true --- # 集成测试方法论 > 参考开源实践:Testcontainers、Testing Library、WireMock、Testcontainers-go、Django Test Client ## 集成测试 vs 单元测试 | 维度 | 单元测试 | 集成测试 | | ---- | -------------- | -------------------- | | 范围 | 单个函数/组件 | 多个模块协作 | | 依赖 | Mock 外部依赖 | 真实依赖(DB、HTTP) | | 速度 | 快(ms) | 较慢(s) | | 目的 | 验证逻辑正确性 | 验证模块间协作 | | 数据 | Mock 数据 | 测试数据库/真实数据 | ## 1. 后端集成测试 ### 1.1 数据库集成测试 使用真实数据库(Testcontainers 或测试数据库),不 mock Repository 层。 #### Node.js + Testcontainers (PostgreSQL) ```typescript // tests/integration/userRepository.test.ts import { PostgreSqlContainer } from '@testcontainers/postgresql'; import { PrismaClient } from '@prisma/client'; import { UserRepository } from '../../src/repositories/userRepository'; describe('UserRepository (integration)', () => { let container: StartedPostgreSqlContainer; let prisma: PrismaClient; let repo: UserRepository; beforeAll(async () => { container = await new PostgreSqlContainer().start(); prisma = new PrismaClient({ datasources: { db: { url: container.getConnectionUri() } } }); await prisma.$executeRawUnsafe('CREATE EXTENSION IF NOT EXISTS "uuid-ossp"'); repo = new UserRepository(prisma); }, 60000); afterAll(async () => { await prisma.$disconnect(); await container.stop(); }, 60000); beforeEach(async () => { await prisma.user.deleteMany(); }); it('creates and retrieves user', async () => { const user = await repo.create({ name: 'Alice', email: 'alice@example.com' }); const found = await repo.findByEmail('alice@example.com'); expect(found?.id).toBe(user.id); expect(found?.name).toBe('Alice'); }); it('returns null for non-existent email', async () => { const found = await repo.findByEmail('nonexistent@example.com'); expect(found).toBeNull(); }); }); ``` #### Python + pytest (PostgreSQL) ```python # tests/integration/test_user_repository.py import pytest from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker from app.models import Base from app.repositories import UserRepository @pytest.fixture(scope="module") def db_session(): engine = create_engine("postgresql://test:test@localhost:5432/test_db") Base.metadata.create_all(engine) Session = sessionmaker(bind=engine) session = Session() yield session session.close() Base.metadata.drop_all(engine) def test_create_and_get_user(db_session): repo = UserRepository(db_session) user = repo.create(name="Alice", email="alice@example.com") found = repo.find_by_email("alice@example.com") assert found.id == user.id assert found.name == "Alice" ``` ### 1.2 API + DB 端到端集成 ```typescript // tests/integration/user-api.test.ts import request from 'supertest'; import { app } from '../../src/app'; import { prisma } from '../../src/db'; describe('User API + DB integration', () => { beforeAll(async () => { await prisma.$connect(); }); afterAll(async () => { await prisma.$disconnect(); }); beforeEach(async () => { await prisma.user.deleteMany(); }); it('POST creates user in database', async () => { const res = await request(app) .post('/api/users') .send({ name: 'Alice', email: 'alice@example.com' }) .expect(201); // 验证数据库中确实存在 const dbUser = await prisma.user.findUnique({ where: { id: res.body.data.id } }); expect(dbUser).not.toBeNull(); expect(dbUser?.name).toBe('Alice'); }); it('GET returns persisted user', async () => { await prisma.user.create({ data: { name: 'Bob', email: 'bob@example.com' } }); const res = await request(app).get('/api/users').expect(200); expect(res.body.data.items).toHaveLength(1); expect(res.body.data.items[0].name).toBe('Bob'); }); it('DELETE removes user from database', async () => { const user = await prisma.user.create({ data: { name: 'Charlie', email: 'charlie@example.com' }, }); await request(app).delete(`/api/users/${user.id}`).expect(200); const dbUser = await prisma.user.findUnique({ where: { id: user.id } }); expect(dbUser).toBeNull(); }); }); ``` ### 1.3 服务间调用集成 使用 WireMock 或 Mock Service Worker 模拟外部服务: ```typescript // tests/integration/order-service.test.ts import { WireMock } from 'wiremock-captain'; describe('OrderService integration with PaymentService', () => { let wiremock: WireMock; beforeAll(async () => { wiremock = new WireMock('http://localhost:8080'); }); it('marks order as paid when payment succeeds', async () => { // Mock 支付服务成功响应 await wiremock.stubFor({ request: { method: 'POST', url: '/payments' }, response: { status: 200, jsonBody: { success: true, transactionId: 'tx-123' } }, }); const order = await orderService.create({ userId: '1', items: [] }); const paid = await orderService.pay(order.id, { method: 'credit_card' }); expect(paid.status).toBe('paid'); }); it('keeps order pending when payment fails', async () => { await wiremock.stubFor({ request: { method: 'POST', url: '/payments' }, response: { status: 402, jsonBody: { error: 'INSUFFICIENT_FUNDS' } }, }); const order = await orderService.create({ userId: '1', items: [] }); await expect(orderService.pay(order.id, { method: 'credit_card' })).rejects.toThrow( 'Payment failed', ); const refreshed = await orderService.getById(order.id); expect(refreshed.status).toBe('pending'); }); }); ``` ## 2. 前端集成测试 ### 2.1 组件 + 状态管理 + API 集成 使用 Testing Library + MSW (Mock Service Worker): ```typescript // tests/integration/UserList.test.tsx import { render, screen, waitFor } from '@testing-library/react'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { UserList } from '../../src/components/UserList'; const server = setupServer( rest.get('/api/users', (req, res, ctx) => res(ctx.json({ data: { items: [{ id: '1', name: 'Alice', email: 'alice@example.com' }] } })) ) ); beforeAll(() => server.listen()); afterEach(() => server.resetHandlers()); afterAll(() => server.close()); describe('UserList integration', () => { it('fetches and displays users from API', async () => { render(); expect(screen.getByText('加载中...')).toBeInTheDocument(); await waitFor(() => { expect(screen.getByText('Alice')).toBeInTheDocument(); }); }); it('shows error when API fails', async () => { server.use( rest.get('/api/users', (req, res, ctx) => res(ctx.status(500))) ); render(); await waitFor(() => { expect(screen.getByText('加载失败')).toBeInTheDocument(); }); }); }); ``` ### 2.2 完整表单提交流程 ```typescript // tests/integration/CreateUserForm.test.tsx import { render, screen, fireEvent, waitFor } from '@testing-library/react'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { CreateUserForm } from '../../src/components/CreateUserForm'; const server = setupServer(); beforeAll(() => server.listen()); afterEach(() => server.resetHandlers()); afterAll(() => server.close()); describe('CreateUserForm integration', () => { it('submits form and shows success', async () => { server.use( rest.post('/api/users', (req, res, ctx) => res(ctx.status(201), ctx.json({ success: true, data: { id: '1' } })) ) ); render(); fireEvent.change(screen.getByLabelText('姓名'), { target: { value: 'Alice' } }); fireEvent.change(screen.getByLabelText('邮箱'), { target: { value: 'alice@example.com' } }); fireEvent.click(screen.getByRole('button', { name: '提交' })); await waitFor(() => { expect(screen.getByText('创建成功')).toBeInTheDocument(); }); }); it('shows validation errors from server', async () => { server.use( rest.post('/api/users', (req, res, ctx) => res(ctx.status(400), ctx.json({ error: { code: 'VALIDATION_ERROR', message: '邮箱格式错误' } })) ) ); render(); fireEvent.change(screen.getByLabelText('姓名'), { target: { value: 'Alice' } }); fireEvent.change(screen.getByLabelText('邮箱'), { target: { value: 'invalid' } }); fireEvent.click(screen.getByRole('button', { name: '提交' })); await waitFor(() => { expect(screen.getByText('邮箱格式错误')).toBeInTheDocument(); }); }); }); ``` ### 2.3 React Context / 状态管理集成 ```typescript // tests/integration/AuthFlow.test.tsx import { render, screen, fireEvent, waitFor } from '@testing-library/react'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { AuthProvider } from '../../src/context/AuthContext'; import { LoginPage } from '../../src/pages/LoginPage'; const server = setupServer(); beforeAll(() => server.listen()); afterEach(() => server.resetHandlers()); afterAll(() => server.close()); describe('Auth flow integration', () => { it('logs in and stores token in context', async () => { server.use( rest.post('/api/auth/login', (req, res, ctx) => res(ctx.json({ data: { token: '', user: { name: 'Alice' } } })) ) ); render( ); fireEvent.change(screen.getByLabelText('邮箱'), { target: { value: 'alice@example.com' } }); fireEvent.change(screen.getByLabelText('密码'), { target: { value: '' } }); fireEvent.click(screen.getByRole('button', { name: '登录' })); await waitFor(() => { expect(screen.getByText('欢迎,Alice')).toBeInTheDocument(); }); }); }); ``` ## 3. 测试数据库策略 ### 3.1 方案选择 | 方案 | 适用场景 | 优点 | 缺点 | | -------------------------- | -------------- | ---------- | ------------------------ | | **Testcontainers** | 需要真实数据库 | 最接近生产 | 启动慢(5-10s) | | **内存数据库** (H2/SQLite) | 简单 CRUD | 快 | 与生产 DB 行为可能不一致 | | **测试数据库** | 已有测试环境 | 真实 | 需要维护 | | **事务回滚** | 单测级隔离 | 快、无残留 | 不支持跨事务测试 | ### 3.2 数据清理策略 ```typescript // 策略 1:每个测试前清理(推荐) beforeEach(async () => { await prisma.user.deleteMany(); await prisma.order.deleteMany(); }); // 策略 2:事务回滚(更快) let tx: Prisma.TransactionClient; beforeEach(async () => { tx = await prisma.$transaction(async (t) => t, { isolationLevel: 'Serializable' }); }); afterEach(async () => { await tx.$rollback?.(); }); ``` ## 4. 集成测试的 Mock 边界 ### 应该 Mock 的 | 依赖 | 原因 | | ------------------------------ | ------------------ | | 第三方 API(支付、邮件、短信) | 不可控、有成本 | | 外部微服务 | 独立部署,不应耦合 | | 文件系统 | 测试速度和隔离 | ### 不应该 Mock 的 | 依赖 | 原因 | | ------------------------- | ------------------------------ | | 数据库 | 集成测试的核心就是验证 DB 交互 | | 应用内部服务 | 要验证模块间协作 | | HTTP 框架(Express/Nest) | 要验证路由和中间件 | | 验证库(Zod/Joi) | 要验证真实校验逻辑 | ## 5. 跨前后端契约集成测试 验证前端发送的 payload 与后端 schema 一致: ```typescript // tests/integration/contract.test.ts import { z } from 'zod'; // 后端定义的 User Schema const CreateUserSchema = z.object({ name: z.string().min(1).max(100), email: z.string().email(), }); // 前端表单提交的数据 const formData = { name: 'Alice', email: 'alice@example.com' }; it('frontend form data matches backend schema', () => { const result = CreateUserSchema.safeParse(formData); expect(result.success).toBe(true); }); ``` ## 6. 集成测试性能优化 ```typescript // 1. 只启动一次数据库容器(beforeAll) let container: StartedPostgreSqlContainer; beforeAll(async () => { container = await new PostgreSqlContainer().start(); }, 120000); // 2. 复用数据库连接 // 3. 数据清理用 TRUNCATE 而非 DROP/CREATE beforeEach(async () => { await prisma.$executeRawUnsafe('TRUNCATE TABLE users, orders RESTART IDENTITY CASCADE'); }); // 4. 并行执行无依赖的测试 // jest.config.js: maxWorkers: '50%' ``` ## 7. 检查清单 后端集成测试: - [ ] 使用真实数据库(Testcontainers 或测试 DB) - [ ] 不 mock Repository/数据访问层 - [ ] 每个测试独立(数据隔离) - [ ] 第三方服务使用 WireMock/MSW mock - [ ] 验证数据真实写入数据库 - [ ] 验证跨模块调用链完整 前端集成测试: - [ ] 使用 MSW mock API,不 mock 内部组件 - [ ] 验证组件 + 状态管理 + API 调用链 - [ ] 覆盖成功和失败场景 - [ ] 验证用户可见的 UI 反馈 - [ ] 不测试实现细节,测试用户行为