Chai 常见陷阱与避坑清单 - 12 个高频错误及解决方案
总览
| 编号 | 陷阱 | 场景 |
|---|---|---|
| 1 | 忘记 .deep 导致对象相等失败 | 对象 / 数组值比较 |
| 2 | .throw 参数没传函数 | 异常捕获 |
| 3 | chai-as-promised 忘记 await / return | 异步 Promise 断言 |
| 4 | should 风格忘记初始化 () | 风格选择 |
| 5 | should 风格断言 null / undefined 直接报错 | should 风格 |
| 6 | 对象字面量在 should 风格下没加括号 | should 风格 |
| 7 | 异步函数传 .throw 不会捕获 Promise reject | 异常 + 异步 |
| 8 | .members 和 .includeMembers 语义混淆 | 数组成员匹配 |
| 9 | 过度使用 .not 导致测试意图模糊 | 否定断言 |
| 10 | .exist 不是 .ok(存在性 vs 真假值) | 值存在性检查 |
| 11 | 忘记 import 插件的副作用 | 插件 ESM 引入 |
| 12 | 链式过长导致 Linter 误报 | 代码风格 |
陷阱 1:忘记 .deep
现象:expect({ a: 1 }).to.equal({ a: 1 }) 报错"expected { a: 1 } to equal { a: 1 }"
原因:Chai 默认用 === 严格相等,对象比较引用不同。
解决:加 .deep 修饰器或用 .eql() 简写:
expect({ a: 1 }).to.deep.equal({ a: 1 });
expect({ a: 1 }).to.eql({ a: 1 }); // 简写
详见:.deep 修饰器
陷阱 2:.throw 参数没传函数
现象:expect(throwError()).to.throw() 直接抛出异常,测试崩溃。
原因:.throw 需要一个函数来调用并捕获异常,直接传表达式会让 JS 立即执行。
解决:包成函数:
// ❌ 错误
expect(throwError()).to.throw();
// ✅ 正确
expect(throwError).to.throw();
expect(() => throwError()).to.throw();
详见:异常捕获
陷阱 3:chai-as-promised 忘记 await / return
现象:异步断言"静默通过"——实际断言没执行,但测试也不报错。
原因:chai-as-promised 的断言返回 Promise,不 await / return 测试就不知道要等。
解决:必须 await 或 return:
// ❌ 忘记 await
it('测试', async function () {
expect(asyncOp()).to.eventually.equal(42);
});
// ✅ 正确
it('测试', async function () {
await expect(asyncOp()).to.eventually.equal(42);
});
陷阱 4:should 风格忘记初始化括号
现象:const should = require('chai').should 后写 foo.should... 报错"should is not a function"。
原因:没执行初始化函数。
解决:加括号执行:
// ❌ 少了 ()
const should = require('chai').should;
// ✅ 正确
const should = require('chai').should();
// 或 ESM
import 'chai/register-should';
详见:should 风格
陷阱 5:should 风格断言 null / undefined
现象:null.should.be.null 直接 TypeError。
原因:null / undefined 没有原型链,没被 should 扩展。
解决:用 should 快捷助手或混和 expect:
const should = require('chai').should();
should.be.null(null); // 助手方式
should.not.exist(err); // 助手方式
// 或临时切到 expect
const { expect } = require('chai');
expect(null).to.be.null;
详见:should 风格限制
陷阱 6:对象字面量在 should 风格没加括号
现象:{ name: 'Chai' }.should.have.property('name') 语法错误。
原因:JS 把 { 开头的表达式当成代码块。
解决:用括号包成表达式:
// ❌ 语法错误
{ name: 'Chai' }.should.have.property('name');
// ✅ 正确
({ name: 'Chai' }).should.have.property('name');
这条也适用于箭头函数返回对象。
陷阱 7:异步函数传 .throw
现象:expect(async () => { throw new Error() }).to.throw() 永远通过。
原因:async 函数本身不抛异常——它返回一个 reject 的 Promise。.throw 检查的是同步抛出。
解决:用 chai-as-promised 的 .rejectedWith:
// ❌ 永远通过
expect(async () => { throw new Error(); }).to.throw();
// ✅ 用 chai-as-promised
await expect(async () => { throw new Error(); }).to.be.rejectedWith(Error);
或者 async/await + try-catch。
陷阱 8:.members vs .includeMembers
现象:断言明明失败了却通过(或反过来)。
原因:两者语义不同:
.members= 两边成员完全一致(集合相等).includeMembers= needle 是 haystack 的子集
expect([1, 2, 3]).to.have.members([1, 2]); // ❌ 失败
expect([1, 2, 3]).to.includeMembers([1, 2]); // ✅ 通过
陷阱 9:过度使用 .not
现象:测试断言太宽泛,导致 bug 被掩盖。
反模式:
// 404、500、302 都会通过
expect(res.status).to.not.equal(404);
// 'bob'、'alice123'、空字符串都可能通过
expect(name).to.not.be.null;
推荐:
expect(res.status).to.equal(200);
expect(name).to.be.a('string').and.have.length.above(0);
陷阱 10:.exist vs .ok
现象:expect(count).to.be.ok 在 count = 0 时误判失败。
原因:
.exist= 非 null 非 undefined.ok= truthy(0、''、false、null、undefined、NaN 都是 falsy)
0 是合法值但不是 truthy。用错了会让测试在 count = 0 时意外失败。
解决:只是确认值存在(不管真假)→ .exist;确认值是 truthy → .ok。
陷阱 11:忘记 import 插件的副作用
现象:TypeScript / ESM 中 chai.use(chaiAsPromised) 报错,或者插件断言不可用。
原因:某些 ESM 构建工具对默认导入和命名导入处理不一致。
解决:
// 方式一:默认导入 + use
import chai from 'chai';
import chaiAsPromised from 'chai-as-promised';
chai.use(chaiAsPromised);
// 方式二:命名导入
import * as chai from 'chai';
import chaiAsPromised from 'chai-as-promised';
chai.default.use(chaiAsPromised);
如果还是不行,看插件 README 里的 ESM 用法说明。
陷阱 12:链式过长 Linter 误报
现象:链式调用超出 ESLint max-len 限制被标记。
解决:
- 调整链式缩进让每行不超长
- 修改
.eslintrc放宽 max-len - 把长链拆成多句断言(更清晰)
// 链式过长(ESLint 报错)
expect(obj).to.have.deep.property('user.profile.settings.theme').that.equals('dark');
// ✅ 拆成两句
expect(obj).to.have.deep.property('user.profile');
expect(obj.user.profile.settings.theme).to.equal('dark');
通用调试技巧
看错误消息
Chai 的错误消息包含实际值和期望值,绝大多数情况下能直接定位:
AssertionError: expected { a: 1 } to deeply equal { a: 2 }
↑ 实际值 ↑ 期望值(前面的 + / - 标记差异)
加自定义消息
expect 风格支持第二参数:
const firstName = response.data.items[0].name;
expect(firstName, '第一个用户的名字').to.be.a('string');
错误消息会变成"第一个用户的名字: expected undefined to be a string"——定位到了嵌套路径返回 undefined。
开启 includeStack
chai.config.includeStack = true;
错误消息追加堆栈,帮助找到自定义 helper 里的最终调用位置。