Chai 中文文档 下载 App

should 风格的已知限制与解决办法

为什么有这些限制

should 风格通过 chai.should() 向 Object.prototype 注入了一个 should getter 属性。这意味着只有那些继承自 Object.prototype 的值才能用 should 链式——null 和 undefined 例外,它们在 JS 中有特殊地位,没有原型链。

限制一:null / undefined 直接 TypeError

// ❌ TypeError: Cannot read properties of null (reading 'should')
null.should.be.null;

// ❌ 同上
undefined.should.be.undefined;

解决方案一:should 全局快捷助手

const should = require('chai').should();

should.be.null(null);
should.be.undefined(undefined);
should.not.exist(err);
should.exist(user);

这些助手也是可链式的(should.not.exist 里 not 就是一个语言链),所以也可以:

should.not.be.null(value);
should.be.ok(response);

解决方案二:混用 expect 风格

团队约定:should 处理已知存在的值,expect 处理可能为 null/undefined 的值:

const { expect } = require('chai');
const should = require('chai').should();

foo.should.be.a('string');        // 已知 foo 存在
expect(err).to.not.exist;        // err 可能为 null / undefined

解决方案三:封装工具函数

function safeShould(value) {
  return {
    beNull: () => { if (value !== null) throw new Error('expected null'); },
    notBeNull: () => { if (value === null) throw new Error('expected not null'); },
    // ...
  };
}

但实际上方案二更简洁实用。

限制二:对象字面量语法陷阱

JS 解析器会把以 { 开头的表达式解释为代码块:

// ❌ SyntaxError: 意外的 token
{ name: 'Chai' }.should.have.property('name');

// ✅ 用括号包成表达式
({ name: 'Chai' }).should.have.property('name');

同样的问题也出现在箭头函数返回对象时。这不是 Chai 的问题,是 JS 语法本身的特性。

记一个规则:should 风格断言对象字面量永远加括号。

限制三:IE9 不兼容

should 风格在 IE9 下不可用。原因是 IE9 的 Object.defineProperty 实现对某些内置原型链节点有缺陷,而且 IE9 不支持 __proto__ 访问器。

这不是 Chai 能修复的。需要支持 IE9 的项目必须用 expect 风格。

should 风格在 IE10+ 和所有现代浏览器上都能正常工作。

限制四:扩展全局原型可能引发困惑

虽然 Chai 的 should 只是一个 getter 属性,不会覆盖已有属性:

const obj = { should: 'a value' };
obj.should;  // 'a value' - 你的属性不会被覆盖

但在某些边缘场景下仍可能引发困惑:

// ❌ Object.create(null) 的对象没有原型
Object.create(null).should.be.an('object');
// TypeError: Object.create(...).should is not a function

解决方案:如果用 Object.create(null) 创建纯字典对象,改用 expect 风格。

限制五:不支持自定义消息参数

expect 风格允许:

expect(answer, '答案不对').to.equal(42);

should 风格不支持这种写法——.should 是属性访问,不是函数调用,无法传参。

如果你需要自定义失败消息,要么切换到 expect 风格,要么在断言外层包 try-catch:

try {
  answer.should.equal(42);
} catch (e) {
  e.message = '答案不对: ' + e.message;
  throw e;
}

但这基本等于给自己找不痛快。需要自定义消息时直接用 expect。

限制六:ESM 初始化不够直观

在 CommonJS 里一行搞定:

require('chai').should();

ESM 里要分两行:

import chai from 'chai';
chai.should();

Chai 提供了一个更简洁的入口:

import 'chai/register-should';

这行导入会自动执行初始化。建议在测试入口文件或 .mocharc.js 的 require 里统一注册。

限制七:TypeScript 严格模式下的类型推断

在 strict: true 的 TypeScript 配置中,对 null 或 undefined 值访问 .should 会被类型检查器报错:

let value: string | null = null;
value.should.be.a('string');
// 编译错误:'value' is possibly 'null'

这实际上是 TypeScript 的设计意图——它在提醒你处理 null 情况。用 expect 风格同样的写法也会报错,但你可以用类型收窄解决:

if (value !== null) {
  value.should.be.a('string');
}

解决策略总结

问题推荐解决方案
null / undefined 断言should 快捷助手或混用 expect
对象字面量永远加括号 ({}).should...
IE9 兼容切换到 expect 风格
Object.create(null)切换到 expect 风格
需要自定义消息切换到 expect 风格
ESM 初始化import 'chai/register-should'
TypeScript strict类型收窄后再 .should

核心建议:新项目直接用 expect 风格,避免踩到 should 的这些限制。已在用 should 的项目,遇到 null / undefined / 自定义消息时临时切到 expect 就行。

下一步