← 中文资源

简体中文版本

Vitest 与 Supertest 的 API 单元测试

用 Vitest 加 Supertest 为 Express 接口写单元测试:工厂函数构建应用实例,覆盖成功、越界与缺参三类路径并断言状态码。

阅读英文原文

示例说明

接口测试的价值在于把契约固化成可执行断言。buildApp 工厂让每个测试拿到干净的应用实例,避免共享状态串扰;Supertest 不需要真实监听端口,直接把 HTTP 请求打进应用对象,速度快且无端口冲突。三个用例分别钉住 201 成功、400 越界与 400 缺参的行为契约。

技术要点

  • 工厂函数保证测试间应用状态隔离
  • Supertest 免端口直连应用对象
  • describe 分组让失败报告可读

代码解读

成功用例断言状态码与响应体字段;越界用例用正则匹配错误文案的关键词而非全文,避免脆弱断言;缺参用例发送空对象触发同一校验分支。beforeAll 只构建一次应用,纯内存路由无需每个用例重建。

使用建议

涉及数据库的接口应注入测试替身而非连真实库。错误文案断言匹配关键词即可,全等断言在文案微调时频繁误报。测试命令纳入 CI 必跑阶段。

代码示例

import { describe, it, expect, beforeAll } from 'vitest';
import request from 'supertest';
import express from 'express';

function buildApp() {
  const app = express();
  app.use(express.json());

  app.post('/feedback', (req, res) => {
    const { rating, comment } = req.body ?? {};
    if (!Number.isInteger(rating) || rating < 1 || rating > 5) {
      return res.status(400).json({ error: 'rating must be 1-5' });
    }
    return res.status(201).json({ received: true, length: comment.length });
  });
  return app;
}

let app;
beforeAll(() => { app = buildApp(); });

describe('POST /feedback', () => {
  it('accepts a valid rating and returns 201', async () => {
    const res = await request(app)
      .post('/feedback')
      .send({ rating: 5, comment: 'great' });
    expect(res.status).toBe(201);
    expect(res.body.received).toBe(true);
  });

  it('rejects out-of-range rating with 400', async () => {
    const res = await request(app)
      .post('/feedback')
      .send({ rating: 9, comment: 'x' });
    expect(res.status).toBe(400);
    expect(res.body.error).toMatch(/rating/);
  });

  it('rejects missing body with 400', async () => {
    const res = await request(app).post('/feedback').send({});
    expect(res.status).toBe(400);
  });
});