expect 断言风格详解 - BDD 链式语言入门
基本形式
const { expect } = require('chai');
expect(actualValue).to.have.methodOrProperty(expectedValue);
expect 接受一个参数(被断言的值),返回一个断言对象,后续通过链式调用构建完整断言。
引入方式
// CommonJS
const { expect } = require('chai');
// ESM
import { expect } from 'chai';
或者直接引用 chai 对象:
const chai = require('chai');
const expect = chai.expect;
链式结构拆解
一条典型的 expect 断言可以拆成三层:
expect('hello').to.be.a('string');
│ │ │ │ │
│ │ │ │ └── 实际断言方法
│ │ │ └─────── 方法参数(期望值)
│ │ └────────── 语言链(修饰)
│ └───────────── 语言链(修饰)
└─────────────────────── 语言链(修饰)
└─────────────────────── 入口函数
语言链里的 to、be、a 这些词只是语法糖,不影响断言逻辑,删掉也一样工作:
// 以下两种写法等价
expect('hello').to.be.a('string');
expect('hello').a('string'); // 省略语言链
但不建议省略——保留语言链让代码读起来像一句自然语言。
字符串类型断言示例
expect('hello').to.be.a('string'); // 类型
expect('hello').to.equal('hello'); // 精确值
expect('Hello').to.ignoreCase.equal('hello'); // 忽略大小写比较
expect('hello world').to.include('world'); // 包含子串
expect('hello world').to.match(/^h/); // 正则匹配
expect('hello').to.have.lengthOf(5); // 长度
数值断言示例
expect(42).to.be.a('number');
expect(42).to.equal(42);
expect(42).to.be.above(40); // 大于
expect(42).to.be.below(50); // 小于
expect(42).to.be.within(40, 50); // 区间 [40, 50]
expect(0.1 + 0.2).to.be.closeTo(0.3, 0.0001); // 浮点近似
expect(42).to.satisfy(n => n % 2 === 0); // 自定义谓词
数组断言示例
const arr = [1, 2, 3];
expect(arr).to.be.an('array');
expect(arr).to.have.lengthOf(3);
expect(arr).to.include(2); // 包含元素
expect(arr).to.include.members([1, 2]); // 包含子集
expect(arr).to.have.members([3, 2, 1]); // 成员完全匹配(无序)
expect(arr).to.have.ordered.members([1, 2, 3]); // 成员完全匹配(有序)
expect(arr).to.not.empty; // 非空
对象断言示例
const obj = { name: 'Chai', version: '5.2.0' };
expect(obj).to.be.an('object');
expect(obj).to.have.property('name');
expect(obj).to.have.property('name', 'Chai'); // 属性 + 值
expect(obj).to.have.all.keys('name', 'version'); // 精确键集合
expect(obj).to.have.any.keys('name', 'author'); // 包含任一键
expect(obj).to.deep.include({ name: 'Chai' }); // 深度部分匹配
expect({ a: { b: 1 } }).to.have.deep.property('a.b', 1); // 嵌套路径
否定:.not
在链式中插入 .not 即可否定后续所有断言:
expect(5).to.not.equal(4);
expect({}).to.not.have.property('name');
expect([1, 2, 3]).to.not.include(4);
但不要把 .not 当成万能药——推荐断言「是什么」而不是断言「不是什么」:
// ✅ 推荐:断言你期望的输出
expect(result.status).to.equal(200);
// ❌ 不推荐:这也可能是 500
expect(result.status).to.not.equal(404);
深度比较:.deep
默认情况下,Chai 的 .equal 使用严格相等(===),所以下面会失败:
expect({ a: 1 }).to.equal({ a: 1 });
// AssertionError: expected { a: 1 } to equal { a: 1 }
加 .deep 后改为深度相等:
expect({ a: 1 }).to.deep.equal({ a: 1 }); // 通过
.deep 会影响后续的 .equal、.include、.members、.keys、.property 等断言。
消息参数
expect(actual, message) 第二个参数会在断言失败时拼到错误信息前面:
expect(user.role, '管理员角色').to.equal('admin');
// AssertionError: 管理员角色: expected 'user' to equal 'admin'
当被断言的值(布尔、数字、null)自身不携带语义时,加个描述性消息特别有用。
与 should 风格的比较
expect 和 should 底层共享同一套 API,只是入口方式不同:
- expect:函数式入口
expect(value) - should:原型注入入口
value.should
在 expect 风格下,你可以断言 null / undefined:
expect(null).to.be.null;
expect(undefined).to.be.undefined;
should 风格对此有限制——详见 should 风格。
与 assert 风格的比较
assert 风格是 TDD 风格,每个断言是函数调用:
// assert 风格
assert.equal(actual, expected);
assert.typeOf(value, 'string');
// expect 风格
expect(actual).to.equal(expected);
expect(value).to.be.a('string');
assert 风格不支持链式调用,功能覆盖与 expect/should 等价。三种风格对比详见 风格对比。
常见误用
忘记写语言链导致语法诡异
// 虽然合法但读起来很怪
expect(foo).equals('bar');
// 推荐,读起来像自然语言
expect(foo).to.equal('bar');
过度使用 .not
// 不推荐
expect(result).to.not.be.null;
expect(result).to.not.be.undefined;
// 推荐
expect(result).to.exist;
深层嵌套消息过长
// 可以但别太长
expect(
response.data.items[0].user.profile.name,
'第一个用户的名字'
).to.be.a('string');
// 不如拆开看
const firstName = response.data.items[0].user.profile.name;
expect(firstName).to.be.a('string');