Chai 中文文档 下载 App

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 风格下,你可以断言 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');

下一步