Chai 插件机制 - chai.use() 扩展断言的三种方式
为什么需要插件
Chai 核心只实现最通用的断言。但很多场景有领域特定的断言需求:
- HTTP 接口测试:需要
.status(200)、.header(...)这类和 HTTP 概念绑定的断言 - Promise 测试:需要
.eventually、.rejectedWith - DOM / 浏览器测试:需要
.dom.text()、.dom.style() - React 组件测试:需要
.to.have.className、.to.contain.text('按钮')
这些场景 Chai 不可能全部内置——太多了,而且每个项目的领域需求都不同。所以 Chai 把扩展权交给社区,通过 .use() 机制让任何人都能写插件。
插件的三种能力
插件可以做三件事:
| 能力 | 示例 | 方法 |
|---|---|---|
| 添加属性断言 | .status(200) / .dom / .jqlite | addProperty / addChainableMethod |
| 添加方法断言 | .throw() / .members([...]) / .equal() | addMethod |
| 修改现有断言 | 覆盖 .equal 的行为 | overwriteMethod / overwriteProperty |
前两种是扩展(加新东西),第三种是修改(改已有东西)。
注册插件
import chai from 'chai';
import chaiHttp from 'chai-http';
import chaiAsPromised from 'chai-as-promised';
// 注册顺序很重要——chai-as-promised 应该最先注册
chai.use(chaiAsPromised);
chai.use(chaiHttp);
chai-as-promised 需要最先注册,因为它会对当前已注册的所有断言做 promisify 包装。如果后续插件注册的断言没被它包到,在 .eventually 里行为可能异常。
插件函数的签名
每个插件是一个函数,Chai 调用它时会传入两个参数:
function myPlugin(chai, utils) {
// chai.Assertion —— 断言构造函数
// chai.Assertion.prototype —— 链式方法挂在这里
// utils 提供了 addMethod / addProperty / overwriteMethod 等工具
}
chai.use() 内部就是执行这个函数一次:
chai.use(myPlugin);
// 等价于
myPlugin(chai, utils);
三种添加方式
addProperty —— 添加属性断言
属性断言不带参数,像 .empty、.ok、.exist 那样:
function myPlugin(chai, utils) {
const Assertion = chai.Assertion;
// 添加 .positive 属性——验证数字 > 0
utils.addProperty(Assertion.prototype, 'positive', function () {
const obj = this._obj;
new Assertion(obj).to.be.a('number'); // 先验证类型
this.assert(
obj > 0,
'expected #{this} to be positive',
'expected #{this} to not be positive'
);
});
}
// 使用
expect(42).to.be.positive;
expect(-1).to.not.be.positive;
addMethod —— 添加方法断言
方法断言带参数,像 .equal()、.members()、.status():
function myPlugin(chai, utils) {
const Assertion = chai.Assertion;
// 添加 .divisibleBy(n) 方法
utils.addMethod(Assertion.prototype, 'divisibleBy', function (n, msg) {
const obj = this._obj;
this.assert(
obj % n === 0,
'expected #{this} to be divisible by #{exp}',
'expected #{this} to not be divisible by #{exp}',
n, // #{exp} 显示的值
obj // #{act} 显示的值(this._obj 自动有,但也可以显式传)
);
});
}
// 使用
expect(42).to.be.divisibleBy(7);
expect(43).to.be.divisibleBy(7); // 失败
addChainableMethod —— 链式方法
链式方法既是方法又是语言链——第一次调用后会变成一个链式 getter:
function myPlugin(chai, utils) {
const Assertion = chai.Assertion;
// 添加 .length(n).above(...) 这种链式
utils.addChainableMethod(
Assertion.prototype,
'length', // 方法名
function (n) { // 作为方法被调用时的行为
const obj = this._obj;
this.assert(
obj.length === n,
'expected #{this} to have length #{exp}',
'expected #{this} to not have length #{exp}',
n
);
},
function () { // 作为链式 getter 被访问时的行为
// 这里可以设置 flag 让后续断言知道当前在断言长度
utils.flag(this, 'length-getter', true);
}
);
}
// 使用
expect([1,2,3]).to.have.length(3); // 作为方法
expect([1,2,3]).to.have.length.above(2); // 作为链式
.length 和 .keys 就是这种模式——可以 .have.lengthOf(3)(方法)也可以 .have.length.above(2)(链式)。
overwriteMethod / overwriteProperty —— 修改已有断言
function myPlugin(chai, utils) {
const Assertion = chai.Assertion;
// 覆盖 .equal 的行为——每次比较前打印日志
utils.overwriteMethod(Assertion.prototype, 'equal', function (_super) {
return function (expected, msg) {
console.log('Comparing', this._obj, 'vs', expected);
return _super.call(this, expected, msg); // 调用原始实现
};
});
}
_super 参数是被覆盖方法的原始实现。用 _super.call(this, ...args) 保证 this 指向正确。
防止重复注册
同一个插件被 use 多次时,可能导致重复注册同一组断言:
// ❌ 重复 use 会导致 .positive 被注册两次
chai.use(myPlugin);
chai.use(myPlugin); // 第二次注册会覆盖第一次,但如果用 overwriteMethod 就可能执行两次
解决方案:在插件内部加一个 flag 检查:
let applied = false;
function myPlugin(chai, utils) {
if (applied) return;
applied = true;
// 正常注册断言...
}
在 ESM 中使用
// 标准做法
import chai, { use } from 'chai';
import chaiHttp from 'chai-http';
chai.use(chaiHttp);
// 也可以
import * as chai from 'chai';
import chaiHttp from 'chai-http';
chai.default.use(chaiHttp);
某些构建工具(Vite / Webpack)处理默认导入和命名导入的方式可能不同。如果遇到 chai.use is not a function,试试 import * as chai from 'chai'。
常见错误
插件函数签名写错
// ❌ 少了 utils 参数
function myPlugin(chai) { /* ... */ }
// ✅ 正确签名
function myPlugin(chai, utils) { /* ... */ }
this 指向错误
在 addMethod / addProperty 的回调里,this 指向当前的 Assertion 实例(也就是 expect(x) 返回的那个对象)。如果你用箭头函数写回调,this 会绑定到外层而不是 Assertion 实例:
// ❌ 箭头函数导致 this 不是 Assertion
utils.addMethod(Assertion.prototype, 'myAssert', () => {
const obj = this._obj; // this 不对!
});
// ✅ 用普通 function
utils.addMethod(Assertion.prototype, 'myAssert', function () {
const obj = this._obj; // this 正确指向 Assertion
});
addMethod / addProperty 顺序
同一个属性名不能既是 addProperty 又是 addMethod。选择哪种取决于它是否需要参数:
- 不需要参数 → addProperty
- 需要参数 → addMethod
- 既要能当属性又能当方法 → addChainableMethod
下一步
- 写一个完整自定义插件:参考本页「三种添加方式」和
chai.use(plugin)注册机制,复用.addMethod/.addProperty编写 - 热门插件推荐 → 热门插件
- chai-http 插件详解 → chai-http 插件