人人都会AI编程

18.2 接口测试:supertest 接口自动化测试

更新时间:2026-07-10

在构建 Web 服务时,单元测试通常针对工具函数、服务层逻辑等纯代码模块,但最终暴露给外部的 HTTP 接口才是系统最直接的功能入口。接口测试通过模拟真实的 HTTP 请求,验证路由处理、中间件、参数校验、响应格式以及业务逻辑的端到端行为,是保障 API 质量的最后一道防线。在 Node.js 生态中,supertest 是最常用的 HTTP 测试工具,它可以与任意测试框架(Jest、Mocha、Vitest 等)无缝集成,以极简的链式语法完成接口的自动化验证。

18.2.1 supertest 的定位与原理

supertest 是一个测试友好的 HTTP 断言库,它封装了 Node.js 原生的 http 模块,能够启动一个临时 HTTP 服务器并绑定你的 Express/Koa/NestJS 等应用实例,然后发起真实的 HTTP 请求。请求不需要通过真实网络栈,而是在进程内部直接传递,因此速度极快,且不会占用系统端口。

核心工作流程:

  1. 在测试文件中引入应用实例(例如 Express 的 app 对象)。
  2. 使用 request(app) 创建一个请求会话,该会话内部会启动一个临时服务器并监听随机端口。
  3. 通过链式调用配置请求方法、路径、请求头、请求体等。
  4. 使用 .expect().end() 进行断言,检查状态码、响应头、响应体是否符合预期。
  5. 每次 request(app) 会独立创建连接,确保测试之间互不干扰。

18.2.2 安装与测试框架集成

安装 supertest 通常作为开发依赖:

npm install --save-dev supertest

supertest 不强制绑定特定测试框架,但实际使用中几乎都会配合 Jest、Mocha 等。以 Jest 为例,一个典型的接口测试文件结构如下:

// app.test.js
const request = require('supertest');
const app = require('../app'); // Express 应用实例

describe('User API', () => {
  test('GET /users 应返回用户列表', async () => {
    const res = await request(app)
      .get('/users')
      .expect(200);

    expect(Array.isArray(res.body)).toBe(true);
  });
});

如果你的应用使用 Koa,只需传入 app.callback() 即可;如果使用 NestJS,则可以在 beforeAll 中初始化 TestingModule 并获取 app.getHttpServer() 传给 supertest。

18.2.3 基本请求与链式断言

supertest 提供了一套流畅的链式 API,覆盖了所有 HTTP 方法:

  • get(path), post(path), put(path), patch(path), delete(path)
  • 设置请求头:set('Authorization', 'Bearer xxx')set({ 'x-custom': 'value' })
  • 发送请求体:send({ username: 'foo', password: 'bar' })
  • 附加查询参数:query({ page: 1, size: 20 })
  • 字段上传(模拟表单):field('name', 'avatar')attach('file', path.join(__dirname, 'test.png'))
  • 设置 Cookie:set('Cookie', 'token=abc123')
  • 期望状态码:expect(200)
  • 期望响应头:expect('Content-Type', /json/)
  • 对响应体进行断言:expect({ status: 'success' })(精确匹配)、expect({ status: 'success' }) 或使用回调函数 expect(res => { ... })

expect() 既可以接收状态码,也可以接收对象/正则,还可以接收一个回调函数对响应对象进行自定义断言。通常我们会混合使用,例如:

const res = await request(app)
  .post('/login')
  .send({ email: 'test@example.com', password: 'secret' })
  .expect(201)
  .expect('Content-Type', /json/);

// 进一步断言响应体
expect(res.body).toHaveProperty('token');
expect(res.body.user.email).toBe('test@example.com');

注意:如果使用 async/await 方式,expect 返回的是响应对象(除非你用回调抛错),可以继续用全功能断言库(如 Jest 的 expect)来验证任何细节。

18.2.4 处理数据库与外部依赖的清理

接口测试通常会与真实数据库交互,为了避免测试间数据污染,需要在每个测试用例前后进行数据准备与清理。推荐做法:

  1. 使用独立的测试数据库:在环境变量中配置单独的数据库连接,每次测试套件启动时执行迁移/重置脚本。
  2. 在每个测试文件或测试用例前清空相关表:可以利用 beforeAllafterAll 钩子执行 truncate 或 delete 操作。
  3. 利用事务回滚:例如在 Prisma 中,可以开启交互式事务,测试结束时回滚。但这种方案实施较复杂,更常用的还是快速清空。

以 Jest 为例:

const { execSync } = require('child_process');

beforeAll(async () => {
  // 迁移测试数据库到最新
  execSync('npx prisma migrate deploy', { env: { ...process.env, DATABASE_URL: 'mysql://test_db...' } });
});

afterEach(async () => {
  // 删除测试产生数据(根据业务调整)
  await prisma.user.deleteMany();
  await prisma.post.deleteMany();
});

如果项目使用 ORM(如 Sequelize、TypeORM),同样可以在 beforeEach 中执行 sync({ force: true }) 或清理特定表。要注意的是,清空操作本身是异步的,必须配合 await 或返回 Promise。

有些项目会使用内存数据库(如 sql.js、mongodb-memory-server)来加速测试,这种方式可以省略数据清理步骤,因为常驻内存不会持久化。但对于复杂查询、存储过程等,内存数据库可能存在表现不一致的问题,需要评估。

18.2.5 认证与权限场景的测试

受保护的接口需要携带认证令牌。一种常见的做法是在 beforeAll 中先通过登录接口获取 token,然后存储到变量中,后续测试复用:

let authToken;

beforeAll(async () => {
  const res = await request(app)
    .post('/login')
    .send({ email: 'admin@test.com', password: 'admin' })
    .expect(201);
  authToken = res.body.token;
});

test('GET /admin/users 需要管理员权限', async () => {
  await request(app)
    .get('/admin/users')
    .set('Authorization', `Bearer ${authToken}`)
    .expect(200);
});

test('缺少 token 应返回 401', async () => {
  await request(app)
    .get('/admin/users')
    .expect(401);
});

对于不需要真实鉴权的单元测试,也可以使用 mock 或直接注入测试用户到中间件,但这会偏离接口端到端测试的初衷。建议在接口测试中使用完整的登录逻辑,以确保认证中间件行为正确。

18.2.6 处理异步接口与流式响应

对于耗时操作(如文件导出、数据库聚合计算),接口可能会需要较长处理时间。supertest 默认的超时时间为 5000ms,可以在测试用例级别增加 Jest 的超时设置:

test('导出大文件', async () => {
  const res = await request(app)
    .post('/export')
    .send({ format: 'csv' })
    .expect(200)
    .timeout(10000);   // 增加 supertest 超时
}, 15000);              // 增加 Jest 测试用例超时

如果你的接口返回流(如 pdf 生成),responsebody 会是 Buffer,可以使用 expect(res => { expect(res.body).toBeInstanceOf(Buffer); }) 来验证,甚至检查文件内容。

18.2.7 常见测试模式与组织建议

在实际项目中,接口测试通常按模块分组,每个资源对应一个测试文件。测试用例应覆盖正面情况、异常输入、边界值和权限场景:

// users.test.js
describe('POST /users', () => {
  test('创建用户成功', async () => {
    const res = await request(app)
      .post('/users')
      .send({ name: 'John', email: 'john@test.com', password: '123456' })
      .expect(201);

    expect(res.body).toMatchObject({ name: 'John', email: 'john@test.com' });
  });

  test('缺少必填字段返回400', async () => {
    await request(app)
      .post('/users')
      .send({ name: 'John' })   // 缺少 email 和 password
      .expect(400);
  });

  test('重复邮箱返回409', async () => {
    // 先创建用户
    await request(app)
      .post('/users')
      .send({ name: 'A', email: 'dup@test.com', password: '123456' });

    // 再次创建相同邮箱
    const res = await request(app)
      .post('/users')
      .send({ name: 'B', email: 'dup@test.com', password: '654321' })
      .expect(409);

    expect(res.body.message).toContain('already exists');
  });
});

这种划分让每个用例职责单一,失败时能快速定位问题。同时,尽量保持测试的独立性:每个用例不依赖其他用例的执行顺序,数据状态由 beforeEachafterEach 保证一致性。

18.2.8 接口测试与单元测试的分工

需要明确的是,接口测试并非单元测试的替代。单元测试验证函数行为,运行极快;接口测试验证 HTTP 层的集成行为,相对较慢。我们应遵循测试金字塔原则:

  • 大部分逻辑使用单元测试覆盖(服务层、工具函数)。
  • 少量核心接口用接口测试覆盖(路由、中间件、参数校验、主要业务流程)。
  • 端到端测试(E2E)覆盖最高层用户交互(如用 Playwright 操作浏览器),数量更少。

supertest 的优势就在于它让接口测试的编写成本接近于单元测试,从而可以更早地发现 API 的回归错误。结合 CI 流水线,每次提交自动运行接口测试,可以有效防止接口契约被意外破坏。


掌握了 supertest 接口测试的基本用法后,我们在下一节将进一步学习集成测试与 E2E 测试的实践,以及如何在复杂的微服务架构中组织多服务的测试策略。