Chai 中文文档 下载 App

Chai 插件机制 - chai.use() 扩展断言的三种方式

为什么需要插件

Chai 核心只实现最通用的断言。但很多场景有领域特定的断言需求:

这些场景 Chai 不可能全部内置——太多了,而且每个项目的领域需求都不同。所以 Chai 把扩展权交给社区,通过 .use() 机制让任何人都能写插件。

插件的三种能力

插件可以做三件事:

能力示例方法
添加属性断言.status(200) / .dom / .jqliteaddProperty / 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。选择哪种取决于它是否需要参数:

下一步