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.prototype 上的所有属性——should 会被枚举。
- 用
Object.create(null)创建的对象没有原型链,自然也没有 should 属性。
// ❌ 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 就行。