示例说明
接口测试的价值在于把契约固化成可执行断言。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);
});
});