chai-http 插件详解 - Express/HTTP 集成测试
定位
chai-http 是 Chai 官方最常用的插件之一,专门用于 HTTP 接口的集成测试。它有两种用法:
- 对本地 Express / Koa / Connect 应用测试:直接传入 app 实例,chai-http 自动处理端口和生命周期。
- 对远程 URL 测试:直接传入 URL 字符串,像普通 HTTP 客户端一样发请求。
安装
npm install chai-http --save-dev
Node.js / CommonJS
const chai = require('chai');
const chaiHttp = require('chai-http');
chai.use(chaiHttp);
const expect = chai.expect;
ESM
import chai from 'chai';
import chaiHttp from 'chai-http';
chai.use(chaiHttp);
基本流程
const chai = require('chai');
const chaiHttp = require('chai-http');
const express = require('express');
chai.use(chaiHttp);
const expect = chai.expect;
// 被测 Express 应用
const app = express();
app.get('/health', (req, res) => res.json({ status: 'ok' }));
// 发起请求 + 断言
it('健康检查应该返回 200', function (done) {
chai.request(app)
.get('/health')
.end((err, res) => {
expect(err).to.be.null;
expect(res).to.have.status(200);
expect(res.body).to.have.property('status', 'ok');
done();
});
});
自动服务器管理
chai-http 最大的便利——对 Express 等应用,你不需要手动 listen / close:
// 自动行为:
// 1. chai.request(app) 时自动调用 app.listen() 找空闲端口
// 2. 请求完成后自动调用 server.close()
// 整个过程对测试透明
这意味着每个 .end() 之后服务器就关闭了,下一个测试会重新启动。
.keepOpen() —— 保持服务器存活
如果你有多个请求要发到同一个服务器(比如先 login 再访问 profile),用 .keepOpen() 避免反复重启:
it('完整流程测试', async function () {
const requester = chai.request(app).keepOpen();
// 第一个请求:登录
const loginRes = await requester.post('/login').send({ username, password });
expect(loginRes).to.have.status(200);
const token = loginRes.body.token;
// 第二个请求:访问资料(带 token)
const profileRes = await requester.get('/profile')
.set('Authorization', `Bearer ${token}`);
expect(profileRes).to.have.status(200);
// 测试结束后手动关闭
requester.close();
});
记得手动调 .close()——你 keepOpen 了就要自己清理。
对远程 URL 测试
it('外部 API 测试', async function () {
const res = await chai.request('https://api.example.com')
.get('/users/123');
expect(res).to.have.status(200);
expect(res.body.name).to.be.a('string');
});
注意:远程 URL 模式只在 Node.js 上工作——chai-http 底层用 Node.js 的 superagent。浏览器端测试请用 chai-superagent。
请求构造方法
chai.request(app) // 或 chai.request(url)
.get('/path') // 或 .post / .put / .patch / .delete / .head
.send({ key: 'val' }) // POST/PUT body
.set('Header', 'value') // 设置请求头
.query({ page: 1 }) // URL 查询参数
.type('json') // Content-Type
.auth(user, pass) // Basic auth
.end(callback) // 回调风格
// 或直接 await(返回 Promise)
常见请求示例
// GET + query params
await chai.request(app)
.get('/users')
.query({ page: 1, limit: 10 });
// POST JSON body
await chai.request(app)
.post('/users')
.set('Content-Type', 'application/json')
.send({ name: 'Alice', email: 'a@b.com' });
// POST form data(x-www-form-urlencoded)
await chai.request(app)
.post('/login')
.type('form')
.send({ username: 'alice', password: '***' });
// 文件上传
await chai.request(app)
.post('/upload')
.attach('file', './test-image.png');
响应断言(chai-http 扩展)
chai-http 扩展了 expect/should 的响应断言,直接对 res 对象调用:
状态码
expect(res).to.have.status(200);
expect(res).to.have.status(404);
expect(res).to.not.have.status(500);
响应头
expect(res).to.have.header('content-type'); // 存在某 header
expect(res).to.have.header('content-type', 'application/json'); // 精确值
expect(res).to.have.header('X-Request-ID', /\w+/); // 正则匹配
header 名称大小写不敏感。
响应体
// JSON body(最常用)
expect(res.body).to.be.an('object');
expect(res.body).to.have.property('data');
expect(res.body.data).to.be.an('array');
// text body
expect(res.text).to.include('Hello World');
// body 存在检查
expect(res).to.not.have.emptyBody;
重定向
expect(res).to.redirect; // 被重定向了
expect(res).to.redirectTo('/new-path'); // 重定向到指定路径
其他
expect(res).to.have.cookie('sessionId');
expect(res).to.have.cookie('sessionId', /^\w+$/);
expect(res).to.have.jsonContentType; // Content-Type 包含 application/json
异步风格选择
chai-http v4+ 原生支持 Promise,推荐用 async/await:
// ✅ 推荐:async/await
it('推荐写法', async function () {
const res = await chai.request(app).get('/users');
expect(res).to.have.status(200);
});
// 也可以:.end(callback)
it('回调写法', function (done) {
chai.request(app).get('/users').end(function (err, res) {
if (err) return done(err);
expect(res).to.have.status(200);
done();
});
});
// 也可以:return Promise
it('return 写法', function () {
return chai.request(app).get('/users').then(function (res) {
expect(res).to.have.status(200);
});
});
完整测试示例
const express = require('express');
const chai = require('chai');
const chaiHttp = require('chai-http');
chai.use(chaiHttp);
const expect = chai.expect;
// 被测应用
const app = express();
app.use(express.json());
let users = [];
app.post('/users', (req, res) => {
const { name } = req.body;
if (!name) return res.status(400).json({ error: 'name required' });
const user = { id: users.length + 1, name };
users.push(user);
res.status(201).json(user);
});
app.get('/users/:id', (req, res) => {
const user = users.find(u => u.id === parseInt(req.params.id));
if (!user) return res.status(404).json({ error: 'not found' });
res.json(user);
});
// 测试套件
describe('Users API', function () {
// 每个测试前清空数据
beforeEach(() => { users = []; });
it('创建用户成功', async function () {
const res = await chai.request(app)
.post('/users')
.send({ name: 'Alice' });
expect(res).to.have.status(201);
expect(res.body).to.have.property('id', 1);
expect(res.body).to.have.property('name', 'Alice');
});
it('创建用户缺少 name 返回 400', async function () {
const res = await chai.request(app)
.post('/users')
.send({});
expect(res).to.have.status(400);
expect(res.body.error).to.include('name');
});
it('查询已创建用户', async function () {
// 先创建
await chai.request(app).post('/users').send({ name: 'Alice' });
// 再查询
const res = await chai.request(app).get('/users/1');
expect(res).to.have.status(200);
expect(res.body.name).to.equal('Alice');
});
it('查询不存在用户返回 404', async function () {
const res = await chai.request(app).get('/users/999');
expect(res).to.have.status(404);
});
});
浏览器端测试
chai-http 的 Node.js 自动服务器管理功能不支持浏览器端。浏览器端测试:
- 用 chai-superagent 插件(chai-http 的浏览器兼容变体)
- 或者直接 fetch + chai 断言
常见陷阱
忘记 .keepOpen() 导致服务器反复重启
如果一个测试里要发多个请求(比如登录→访问),每个请求都会重启服务器。用 .keepOpen() 保持服务器存活。
忘记 requester.close()
用了 .keepOpen() 后必须手动 .close(),否则服务器进程泄漏。
.end(err, res) 里的 err
chai.request(app).get('/path').end((err, res) => {
// ❌ 写法1:err 可能是 null
if (err) throw err;
// ❌ 写法2:res 可能是 null
expect(res.status).to.equal(200);
});
当请求网络层出错时,err 有值、res 为 null。业务层返回 4xx/5xx 时,err 通常是 null、res 有值(含错误状态码)。
所以正确的拆分:
- 网络错误 →
err非 null,用if (err) return done(err) - HTTP 错误(4xx/5xx) →
res.status是错误码,expect(res).to.have.status(200)会失败
浏览器端用 chai-http 会报错
chai-http 的自动服务器管理依赖 Node.js 的 http 模块。浏览器端请改用 chai-superagent 或直接 fetch。
下一步
- 插件基础机制 → 插件基础
- chai-as-promised → chai-as-promised
- 实战场景 → 13-实战场景