人人都会AI编程

18.3 集成测试与 E2E 测试

更新时间:2026-07-10

单元测试验证的是函数或模块在隔离环境下的正确性,接口测试确保单个 HTTP 端点能够返回预期响应。但当我们需要验证多个模块协同工作时的行为(比如数据库写入后查询、消息队列的生产与消费),或者从用户视角走完一个完整的业务流程时,就需要集成测试(Integration Test)和端到端测试(E2E Test)。

18.3.1 集成测试:让多个真实模块一起跑

集成测试的目标是暴露模块之间交互时可能出现的错误,这些错误往往是单元测试的 mock 环境下难以发现的。在 Node.js 项目中,典型的集成测试会启动一个真实的(或轻量化的)数据库实例,而非 mock,然后调用服务层或路由处理逻辑,验证数据确实被正确持久化。

与单元测试的核心区别:

| 单元测试 | 集成测试 |
|---------|---------|
| 隔离被测单元,mock 所有外部依赖 | 使用真实的依赖(数据库、消息队列、文件系统等) |
| 执行速度极快(毫秒级) | 较慢(数百毫秒到秒级),因为涉及 I/O 与网络 |
| 定位问题精确到某个函数 | 验证模块间的协作逻辑是否正确 |
| 适合快速反馈,覆盖边界用例 | 适合验证关键业务流程、数据一致性 |

实践方案:测试数据库

集成测试最常用的外部依赖就是数据库。为了确保测试的可重复性和独立性,通常为测试环境准备一个独立的数据库实例(本地 MySQL/Postgres 或 Docker 容器),并在每个测试用例前后执行如下操作:

  • 全局初始化(beforeAll):执行数据库迁移脚本,创建表结构。
  • 每个用例前(beforeEach):清空所有表,或回滚到一个已知状态,防止用例间数据污染。
  • 用例执行:调用实际的业务函数或路由,然后直接查询数据库校验结果。
  • 全局清理(afterAll):关闭数据库连接。

这里以用户注册为例,展示一个集成测试用例(使用 Jest + Prisma):

// __tests__/integration/user.test.ts
import { createUser, findUserByEmail } from '../../services/user.service';
import prisma from '../../lib/prisma';

beforeAll(async () => {
  // 确保数据库表已存在,可以使用 Prisma 的 migrate deploy
  // 这里省略具体迁移逻辑
});

beforeEach(async () => {
  // 清空用户表,避免测试间相互影响
  await prisma.user.deleteMany();
});

afterAll(async () => {
  await prisma.$disconnect();
});

test('createUser 应成功创建用户并持久化到数据库', async () => {
  const userData = { email: 'test@example.com', password: '123456' };
  const user = await createUser(userData);

  // 验证返回的用户对象
  expect(user.email).toBe(userData.email);
  expect(user.password).not.toBe('123456'); // 应被加密

  // 直接从数据库查询,确认数据已写入
  const dbUser = await findUserByEmail('test@example.com');
  expect(dbUser).not.toBeNull();
  expect(dbUser!.email).toBe(userData.email);
});

这个例子没有 mock prisma,而是直接连接本地测试库。createUser 内部会使用 bcrypt 加密密码并写入数据库,集成测试完整覆盖了这个过程。

集成测试的“度”:不追求全覆盖

集成测试相对耗时较长,不宜追求与单元测试同样的覆盖率。通常只用于覆盖关键业务流(如订单创建、扣减库存、用户注册),其余场景用更便宜的单元测试或接口测试覆盖。


18.3.2 E2E 测试:端到端验证用户旅程

E2E 测试站在用户角度,通过真实的浏览器或 HTTP 客户端完整模拟用户操作,验证整个系统(前端 + 后端 + 数据库 + 外部服务)是否按预期工作。它不关心内部实现细节,只检查最终结果是否正确。

在 Node.js 生态中,E2E 测试通常分为两类:

  1. 纯 API 驱动的 E2E:使用 supertest 等 HTTP 客户端,对真实运行的服务发起请求,验证状态码、响应体和副作用(如数据库变化)。本质上是用 HTTP 请求模拟客户端行为,适合前后端分离的 API 项目。
  2. 浏览器自动化 E2E:使用 Playwright、Cypress 或 Puppeteer 控制真实浏览器,操作页面元素并断言 UI 状态,适合包含前端交互的完整应用。

纯 API 的 E2E 测试示例

假设我们在本地启动了完整的服务器(包括数据库和缓存),可以直接用 supertest 发起请求:

import request from 'supertest';
import app from '../../app'; // Express/Koa 应用实例

test('用户注册流程:注册 -> 登录 -> 获取个人信息', async () => {
  const email = 'e2e@example.com';
  const password = 'test1234';

  // 1. 注册新用户
  const registerRes = await request(app)
    .post('/api/register')
    .send({ email, password })
    .expect(201);
  expect(registerRes.body.user.email).toBe(email);

  // 2. 登录获取 Token
  const loginRes = await request(app)
    .post('/api/login')
    .send({ email, password })
    .expect(200);
  const token = loginRes.body.token;

  // 3. 使用 Token 获取个人信息
  const profileRes = await request(app)
    .get('/api/me')
    .set('Authorization', `Bearer ${token}`)
    .expect(200);
  expect(profileRes.body.email).toBe(email);
});

这种测试虽然比单元测试慢,但能发现路由配置、中间件顺序、序列化错误等真实环境中才会暴露的问题。

浏览器自动化 E2E 测试(Playwright 示例)

对于有前端界面的系统,E2E 测试会启动 Node.js 服务,然后用 Playwright 打开 Chrome 执行操作:

const { chromium } = require('playwright');

let browser, page;
beforeAll(async () => {
  browser = await chromium.launch();
  page = await browser.newPage();
  // 假设应用启动在 localhost:3000
  await page.goto('http://localhost:3000');
});

afterAll(async () => {
  await browser.close();
});

test('用户可以通过注册表单完成注册', async () => {
  await page.click('text=注册');
  await page.fill('input[name="email"]', 'test@example.com');
  await page.fill('input[name="password"]', '123456');
  await page.click('button[type="submit"]');

  // 等待跳转到首页,验证用户名显示
  await page.waitForSelector('text=欢迎, test');
  const text = await page.textContent('.welcome-message');
  expect(text).toContain('test');
});

E2E 测试的维护成本与策略

E2E 测试速度慢、依赖完整环境,容易因 UI 微调而失败,因此实践中应遵循“测试金字塔”原则:大量单元测试,少量集成测试,极少但关键的 E2E 用例。通常只对核心用户路径(如登录、支付、注册)编写 E2E 测试。

为了提升稳定性,可以采取以下措施:

  • 使用 Docker Compose 管理测试环境,确保每次 E2E 运行时环境一致。
  • 使用独立的测试数据库和第三方服务沙箱(如 Stripe test mode)。
  • 在 CI 中按需运行 E2E 测试,例如仅当 Pull Request 修改了关键服务时才触发。
  • 对浏览器自动化测试,使用 data-testid 属性选择元素,避免依赖 CSS 类或文本内容的变化。

集成测试与 E2E 测试是软件质量保障的最后防线。集成测试确保内部模块间的契约正确,E2E 测试确保最终交付的功能符合用户期望。与单元测试和接口测试结合在一起,它们共同构建了一套从微观到宏观、从静态到动态的完整测试体系。