Chai 中文文档 下载 App

chai-http 插件详解 - Express/HTTP 集成测试

定位

chai-http 是 Chai 官方最常用的插件之一,专门用于 HTTP 接口的集成测试。它有两种用法:

  1. 对本地 Express / Koa / Connect 应用测试:直接传入 app 实例,chai-http 自动处理端口和生命周期。
  2. 对远程 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 自动服务器管理功能不支持浏览器端。浏览器端测试:

常见陷阱

忘记 .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 有值(含错误状态码)。

所以正确的拆分:

浏览器端用 chai-http 会报错

chai-http 的自动服务器管理依赖 Node.js 的 http 模块。浏览器端请改用 chai-superagent 或直接 fetch。

下一步