should 断言风格详解 - 原型注入 BDD 链式
基本形式
require('chai').should();
foo.should.be.a('string');
foo.should.equal('bar');
与 expect 的核心区别:expect 是函数式入口 expect(value),should 是原型注入入口 value.should。
初始化
should 风格需要显式调用初始化函数(注意是执行函数,不是引用):
const should = require('chai').should();
容易犯的错:忘记加括号 ():
// ❌ 这样不会生效——你只是拿到了函数的引用,没执行它
const should = require('chai').should;
// ✅ 正确
const should = require('chai').should();
ES2015 模块下的写法:
import chai from 'chai';
chai.should();
// 或者更简洁的一行
import 'chai/register-should';
语法对比 expect
两者底层完全相同,只是入口不同:
// expect 风格
expect('hello').to.be.a('string');
expect([1, 2, 3]).to.have.lengthOf(3);
expect({ name: 'Chai' }).to.have.property('name');
// should 风格(去掉外层 expect(value),改成 value.should)
'hello'.should.be.a('string');
[1, 2, 3].should.have.lengthOf(3);
({ name: 'Chai' }).should.have.property('name');
注意对象字面量需要用括号包起来,否则 JS 会把花括号当成代码块:
// ❌ 语法错误——JS 把 { name: 'Chai' } 解析成代码块
{ name: 'Chai' }.should.have.property('name');
// ✅ 用括号包起来
({ name: 'Chai' }).should.have.property('name');
should 风格独有的快捷助手
由于 should 扩展了 Object.prototype,它也提供了几个全局快捷助手:
const should = require('chai').should();
// 这些可以直接用在 null / undefined 场景(后面会讲为什么需要)
should.not.exist(err);
should.exist(user);
should.equal(result.statusCode, 200);
should.throw(() => undefined.foo());
这些助手在处理可能为 null / undefined 的值时特别有用——因为 null 和 undefined 不会被扩展原型。
null / undefined 的处理痛点
should 风格有一个绕不开的问题:null 和 undefined 没有原型,所以 null.should 会直接报错:
// ❌ 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);
方案二:混入 expect 风格
const { expect } = require('chai');
const should = require('chai').should();
// 大部分情况用 should
foo.should.be.a('string');
// 遇到 null / undefined 场景切换到 expect
expect(err).to.not.exist;
团队内可以约定一条规则:断言 null / undefined 时用 expect,其他情况用 should。
初始化时机
should 只需要初始化一次。在 Mocha 的 .mocharc.js 或 Jest 的 setup 文件里统一初始化:
// .mocharc.js
module.exports = {
require: ['chai/register-should']
};
这样所有测试文件自动生效,不用每个文件单独写 require('chai').should()。
与 expect 共享的 API
除了入口不同,should 和 expect 底层共享所有断言方法和修饰器。.deep、.not、.include、.property 等用法完全一致。
链式结构对比:
// expect
expect(obj).to.deep.include({ key: 'val' });
// should
obj.should.deep.include({ key: 'val' });
语言链 to、be、a、have 等在 should 中同样有效。
ES2015 模板字符串的陷阱
模板字符串如果只包含一个表达式,结果就是这个表达式本身:
const x = 'hello';
`${x}` === 'hello'; // true
`${x}`.should.be.a('string'); // 合法,但不太常见
这没什么问题,知道就行。
什么时候选 should
should 风格的优势:
- 自然语言感更强:
foo.should.equal(bar)读起来像一句完整的句子。 - 不需要在顶部声明变量:不需要
const expect = chai.expect。 - IDE 补全友好:
foo.should.之后 IDE 能补全所有可用断言。
should 风格的劣势:
- 修改原型:虽然 Chai 的
should属性只是一个 getter,不会破坏现有属性,但在某些场景下(如库代码、TypeScript strict 模式)可能引发问题。 - null / undefined 要额外处理:不像 expect 风格那样干净。
- IE9 不兼容:
Object.defineProperty在旧 IE 中有坑。 - 对象字面量要加括号:
({}).should...略显累赘。
与类型系统的配合
TypeScript 中 should 风格需要类型声明文件的支持。Chai v4+ 自带了完整的类型声明,安装后 TypeScript 能正确识别 .should 扩展的属性。
下一步
- 对比 assert 风格 → assert 风格
- should 风格的更多限制 → should 风格限制
- 三种风格整体对比 → 风格对比