Chai 断言库速查表 - Chai.js 测试断言 expect/should/assert 大全
这张速查给用 Mocha、Jest 集成或纯 Node 写 JavaScript 测试的开发者。Chai 提供了 expect、should、assert 三种可互换的风格,这张表会把同一种检查用不同写法各展示一遍。内容覆盖相等与对象/数组的深度比较、类型与包含检查、抛错断言、数值区间,以及依赖插件的助手方法,比如 eventually(chai-as-promised)和 HTTP 状态码(chai-http)。读完你应能对值、集合、抛错和 Promise 写出清晰易读的断言,并明白什么时候该用 deep.equal 而不是 equal。
典型使用场景
面向用 BDD 风格描述业务规则的前端与 Node.js 测试作者。三种断言风格里 expect 最通用、读起来最接近自然语言,should 写起来最短但对空值有坑,assert 属于 TDD 风格且函数式调用更显式。本表把这三种风格并列收录,让你在同一个仓库里遇到哪种都能读懂;重点覆盖相等与深度比较的选择(.equal 走严格相等、.deep.equal 逐层比较)、类型与包含断言、异常断言的四种写法(按类型、按消息正则、按不抛错),以及需要插件才生效的 Promise 与 HTTP 断言。排障部分解释了 ? 为什么显示为 ??、.own.property 与 .property 的区别、should 遇到 null 崩溃等真实会中断测试套件的问题。
Expect 风格(BDD) 8
expect(value).to.equal(expected)expect(value).to.be.a("string")expect(array).to.include(item)expect(value).to.be.trueexpect(value).to.not.be.nullexpect(array).to.have.lengthOf(3)expect(value).to.existexpect(obj).to.have.property("key", "value")Should 风格(BDD) 5
value.should.equal(expected)value.should.be.a("string")array.should.include(item)value.should.be.truevalue.should.existAssert 风格(TDD) 7
assert.equal(actual, expected)assert.notEqual(actual, expected)assert.deepEqual(actual, expected)assert.isTrue(value)assert.isArray(value)assert.throws(fn)assert.fail(actual, expected, "message")相等与深度比较 6
expect(value).to.deep.equal(obj)expect(value).to.eql(obj)expect(value).to.not.equal(other)expect(obj).to.have.own.property("key")expect(obj).to.have.nested.property("a.b.c")expect(arr).to.deep.equal([1, 2, 3])类型与包含断言 6
expect(value).to.be.an("array")expect(value).to.be.an.instanceof(Date)expect(str).to.match(/^foo/)expect(str).to.have.string("bar")expect(obj).to.include({ key: "value" })expect(arr).to.have.members([1, 2, 3])异常与抛错 6
expect(fn).to.throw(Error)expect(fn).to.throw("error message")expect(fn).to.throw(/message/)expect(fn).to.not.throw()assert.throws(fn, TypeError)assert.doesNotThrow(fn)数字、布尔与插件 6
expect(num).to.be.above(5)expect(num).to.be.at.least(5)expect(num).to.be.below(10)expect(num).to.be.within(1, 10)expect(promise).to.eventually.equal("resolved")expect(res).to.have.status(200)参数矩阵
| 参数 | 作用 | 示例 |
|---|---|---|
expect(value).to.equal(x) | 严格相等断言(===) | expect(value).to.equal(expected) |
expect(value).to.be.a("string") | 类型断言 | expect(value).to.be.a("string") |
expect(arr).to.include(item) | 包含某个元素断言 | expect(array).to.include(item) |
expect(value).to.be.true | 布尔真值断言 | expect(value).to.be.true |
expect(value).to.not.be.null | 非空断言 | expect(value).to.not.be.null |
expect(arr).to.have.lengthOf(3) | 长度断言 | expect(array).to.have.lengthOf(3) |
expect(value).to.exist | 存在断言(非 null 且非 undefined) | expect(value).to.exist |
expect(obj).to.have.property("k", "v") | 断言属性存在且值匹配 | expect(obj).to.have.property("key", "value") |
value.should.equal(x) | should 风格相等断言 | value.should.equal(expected) |
value.should.exist | should 风格存在断言 | value.should.exist |
assert.equal(a, b) | 宽松相等比较(==) | assert.equal(actual, expected) |
assert.notEqual(a, b) | 宽松不等断言 | assert.notEqual(actual, expected) |
assert.deepEqual(a, b) | 递归比较对象/数组每一层内容 | assert.deepEqual(actual, expected) |
assert.isTrue(v) / assert.isArray(v) | 布尔与数组类型断言 | assert.isArray(value) |
assert.throws(fn) | 断言执行时会抛异常 | assert.throws(fn) |
assert.fail(a, b, "message") | 无条件让测试失败,用于不可到达分支 | assert.fail(actual, expected, "message") |
.to.deep.equal(obj) | 深度比较对象/数组 | expect(value).to.deep.equal(obj) |
.to.eql(obj) | .deep.equal 的别名 | expect(value).to.eql(obj) |
.to.not.equal(other) | 不相等断言 | expect(value).to.not.equal(other) |
.to.have.own.property("k") | 自有属性断言,不含原型链 | expect(obj).to.have.own.property("key") |
.to.have.nested.property("a.b.c") | 嵌套路径属性断言 | expect(obj).to.have.nested.property("a.b.c") |
.to.be.an("array") | 数组类型断言 | expect(value).to.be.an("array") |
.to.be.an.instanceof(Date) | 实例类型断言 | expect(value).to.be.an.instanceof(Date) |
.to.match(/^foo/) | 正则匹配断言 | expect(str).to.match(/^foo/) |
.to.have.string("bar") | 字符串包含子串断言 | expect(str).to.have.string("bar") |
.to.include({ key: "value" }) | 对象部分包含断言 | expect(obj).to.include({ key: "value" }) |
.to.have.members([1, 2, 3]) | 数组成员完全相等断言 | expect(arr).to.have.members([1, 2, 3]) |
.to.throw(Error) | 断言抛出指定类型异常 | expect(fn).to.throw(Error) |
.to.throw("error message") | 断言异常消息包含该子串 | expect(fn).to.throw("error message") |
.to.throw(/message/) | 用正则匹配异常消息 | expect(fn).to.throw(/message/) |
.to.not.throw() | 断言不抛异常 | expect(fn).to.not.throw() |
assert.throws(fn, TypeError) | TDD 风格抛错断言 | assert.throws(fn, TypeError) |
assert.doesNotThrow(fn) | TDD 风格不抛错断言 | assert.doesNotThrow(fn) |
.to.be.above(5) / .to.be.below(10) | 大于 / 小于断言 | expect(num).to.be.above(5) |
.to.be.at.least(5) | 大于等于断言 | expect(num).to.be.at.least(5) |
.to.be.within(1, 10) | 区间断言,含边界 | expect(num).to.be.within(1, 10) |
.to.eventually.equal("resolved") | Promise 断言,需 chai-as-promised 插件 | expect(promise).to.eventually.equal("resolved") |
.to.have.status(200) | HTTP 状态断言,需 chai-http 插件 | expect(res).to.have.status(200) |
易错点与避坑指南
现象对可能是 null 的值用 should 风格断言,测试直接崩在读 .should 上,套件中断。
原因should 写法需要先对对象取属性,null/undefined 在断言执行前就抛错。
处置改用 expect 风格:expect(value).to.not.be.null,断言函数会先安全地接收空值。
现象对象明明字段值一致,.to.equal 却失败。
原因.equal 走的是严格相等,只对引用类型比较地址,两个内容相同但不同引用的对象不会通过。
处置改用 .to.deep.equal 或 .eql 逐层比较内容。
现象数组断言「看起来是同一个数组」却依然失败。
原因严格相等比较的是引用,测试里重新构造的数组与被测对象里的数组不是同一个引用。
处置用 expect(arr).to.deep.equal([1, 2, 3]) 做内容比较,或用 .have.members 只比较成员集合。
现象想断言「对象上有这个属性」,结果原型链上的属性也算通过。
原因.property 会遍历原型链,而判断自有属性应该用 .own.property。
处置改用 expect(obj).to.have.own.property("key"),只认对象自身的属性。
现象报错信息里的 expected 是 ??,看不出到底在比什么。
原因传给期望值的表达式本身求出了 undefined,Chai 只能把 undefined 渲染成占位符。
处置把期望值先算好再传入,或给断言补上自定义 message,确保比较的是确定的值。
现象用 .to.throw() 断言时函数其实抛了另一个错,却仍然通过。
原因不传参数时只校验「有没有抛错」,不校验类型和消息。
处置给出具体期望:.to.throw(Error) 校验类型,.to.throw("message") 或 .to.throw(/message/) 校验消息内容。
现象断言 Promise 解析结果时报 eventually is not a function。
原因Promise 相关断言依赖 chai-as-promised 插件,插件未注册。
处置在测试入口注册 chai-as-promised 后再使用 expect(promise).to.eventually 等链式写法。
排障路径
1先确认值的实际类型再选断言
expect(value).to.be.a("string")类型断言失败时会直接打印实际类型,比猜断言写法更快定位问题。
2区分浅比较与深比较
expect(value).to.deep.equal(obj)内容一致但 .to.equal 失败,就说明应该换成深比较。
3只比较数组成员而不关心顺序
expect(arr).to.have.members([1, 2, 3])成员集合相同时通过,顺序不同也能通过。
4校验错误分支确实抛出了指定异常
expect(fn).to.throw(TypeError)把被测函数包进断言执行,未抛出或类型不符都会直接失败并打印实际错误。
5确认属性是自有属性而非原型链继承
expect(obj).to.have.own.property("key")若 .property 通过而 .own.property 失败,说明该属性来自原型链。
6断言 HTTP 响应状态
expect(res).to.have.status(200)需先注册 chai-http 插件;失败时输出实际状态码。
命令示例
BDD 三段式断言:类型 + 内容 + 空值一次写全
const res = await parse(body);
expect(res).to.be.an("object");
expect(res.data).to.have.lengthOf(3);
expect(res.error).to.not.exist;
expect(res.data[0].name).to.equal("tom");Chai 的 BDD 风格是链式的,expect 只写一次,后面 .to / .be / .have 都可以换着用,断言信息会自动拼成可读的路径。比分别写四条断言更容易看出「哪个属性出的问题」。
深度比较对象/数组:deep.equal 与 eql
expect(actual).to.deep.equal({ id: 1, tags: ["a", "b"] });
// eql 是 deep.equal 的别名,风格更短
expect(rows).to.eql([{ id: 1 }, { id: 2 }]);
// 数组要求成员完全一致,顺序无关
expect(ids).to.have.members([3, 1, 2]);equal 是严格 ===,两个内容相同但引用不同的对象一定失败;比结构必须用 deep.equal / eql。members 则要求集合成员完全相同(可乱序),适合断言「恰好这几条,没有多余项」。
测试错误分支:throws 的三种匹配方式
expect(() => JSON.parse("{")).to.throw();
expect(() => parse("x")).to.throw(TypeError);
expect(() => parse("x")).to.throw(/invalid/);
// 不该抛异常的分支也要断言,否则静默通过
expect(() => parse("{}")).to.not.throw();只写 to.throw() 只能证明「抛了东西」,写错错误类型或错误消息的 bug 一样能过。按「异常类型 + 消息正则」收紧,并在成功路径补一条 to.not.throw(),才能真正覆盖双向分支。
只断言对象里关心的几个字段:include
expect(user).to.include({ role: "admin" });
expect(user).to.have.own.property("id");
expect(user).to.have.nested.property("profile.address.city");include 做部分包含匹配,新增字段不会让测试红;own.property 只看自有属性,继承来的也算「存在」会误判;nested.property 用点号走嵌套路径,任一层缺失都会给出明确报错位置。
数字区间与异步:within / eventually / status
expect(score).to.be.within(0, 100);
expect(save()).to.eventually.equal("ok");
// 需先注册 chai-as-promised / chai-http
// expect(res).to.have.status(200);within 两端都含边界,避免手写 above(0).and.below(100) 漏掉边界值。eventually / status 依赖 chai-as-promised、chai-http 这类插件,不注册就直接报找不到断言方法。
常见坑与注意事项
- expect(value).to.equal(other) 用的是严格 ===,两个内容相同但引用不同的对象/数组一定失败;比结构时忘记改成 deep.equal,会出现「明明一样却不通过」的假故障。
- BDD 链式断言一旦中间某个属性为 undefined,后面的 .to.have.property 会连带炸出一串无关报错。排查时从链首开始逐段缩小,不要一次改多处。
- assert.equal 用宽松 == 比较,"1" 与 1 会被判为相等,字符串与数字混用的字段用它会漏掉类型错误;优先用严格相等的断言。
- expect(fn).to.throw() 只验证「抛了异常」,不校验类型和消息,业务抛错类型写错也能测试通过——错误分支必须同时约束类型与消息。
- Promise 断言必须先注册 chai-as-promised,否则 eventually / rejected 这些属性不存在,报错是「断言方法不是函数」,与业务代码无关,容易误判为测试环境问题。
- 只写 expect(fn).to.throw() 而不给成功路径补一条「不抛异常」的断言时,出错分支被改坏(该抛的不抛)完全不会被测出来。
提示
- expect 和 should 是 BDD 风格,assert 是 TDD 风格。
- should 需要对对象调用 .should,可能对 null/undefined 报错。
- 深度比较用 .deep.equal,浅比较用 .equal。
- chai-as-promised 插件支持 Promise 断言,chai-http 支持 HTTP 请求断言。
- 链式方法如 to、be、have 仅提高可读性,无实际功能。
常见问题
什么时候该选 should,什么时候该选 expect?
expect(value).to.equal(expected) 把被测对象作为第一个参数传入,断言写在后面;should 风格是 value.should.be.a("string"),断言挂在被测对象上。差别有两点:一是代码风格,BDD 读起来更顺,expect 写起来对 undefined/null 更安全;二是空值处理——should 是给对象加属性,对 null 和 undefined 调用 .should 会直接报错,所以断言可空值时必须用 expect(value).to.not.be.null 这种 expect 形式。assert 属于 TDD 风格,写作 assert.equal(actual, expected),更接近单元测试框架的经典形态。一个项目里选定一种并保持一致即可。
equal 和 deep.equal 到底差在哪一层?
expect(value).to.equal(expected) 是严格相等(===),比较的是引用或原始值:expect([1,2]).to.equal([1,2]) 会失败,因为这是两个不同的数组对象。expect(value).to.deep.equal(obj)(简写 .to.eql)才逐层递归比较内容,数组、嵌套对象都能匹配。另外 expect(obj).to.have.own.property("key") 只看自有属性、不查原型链,expect(obj).to.have.nested.property("a.b.c") 支持路径式断言。排查「明明内容一样却断言失败」时,先确认是不是漏了 deep——这是最常见的原因。
测「错误分支」时,怎么断言一个函数确实抛了错?
expect 风格有四种粒度:expect(fn).to.throw(Error) 只看异常类型,expect(fn).to.throw("error message") 要求消息全等,expect(fn).to.throw(/message/) 用正则匹配消息的一部分,expect(fn).to.not.throw() 断言正常路径。关键点是必须传函数本身而不是调用结果——写成 expect(fn()).to.throw() 会先执行 fn,异常在断言之前就抛出去了。assert 风格对应 assert.throws(fn, TypeError) 和 assert.doesNotThrow(fn)。断言不支持到达的分支时用 assert.fail(actual, expected, "message") 让测试无条件失败,比悄悄通过更安全。
测试异步接口返回的 Promise,chai 要怎么配?
chai 核心不含异步断言,需要插件。最常用的是 chai-as-promised,它给 expect 加上了 eventually 修饰:expect(promise).to.eventually.equal("resolved") 会等 Promise 落定后再断言结果,配合 rejected 则用于断言失败原因,形式类似 expect(promise).to.be.rejectedWith(Error)。HTTP 状态断言则由 chai-http 提供,expect(res).to.have.status(200) 直接校验响应码和响应体。若项目已使用 Jest 的 expect,这两类断言通常由 Jest 自带的 resolves / rejects 覆盖,无需再引 chai 插件。
include 和 members 断言数组,结果差在哪?
expect(arr).to.include(item) 只要求数组中存在该元素,多余元素不影响判定,适合「至少包含用户要找的那一项」。expect(arr).to.have.members([1, 2, 3]) 要求成员完全一致、个数和值都要对得上,适合快照式的整体校验。另外 expect(obj).to.include({ key: "value" }) 是对象部分包含,只比对指定键,不要求对象整体相等。字符串有专门的 expect(str).to.have.string("bar") 做子串匹配,而数字比较则用 above / below / at.least / within(1, 10) 这组。选错会得到过松或过严的断言。
由 巧匠 维护
公开更新于 2026年7月21日,内容持续校对官方文档。