Chai vs Jest expect 对比 - 该选哪个断言库
一句话结论
- 用 Jest 做测试运行器 → 直接用 Jest 自带的
expect,没必要同时引入 Chai。 - 用 Mocha / Vitest / Karma 等其他运行器 → Chai 是最成熟、功能最丰富的断言库。
- Vitest 用户 → Vitest 也内置了与 Jest expect 几乎完全相同的 API。
背景
Jest 是 Facebook 开源的一体化测试框架——它把测试运行器 + 断言库 + Mock + 快照测试 + 覆盖率工具打包在一起。jest.expect 是 Jest 内置的断言接口,风格和 Chai 的 .not / .deep 很像。
这就导致一个常见问题:如果我用 Jest,还有必要用 Chai 吗?
风格对比
链式 BDD vs. 方法调用
// Chai(expect 风格)—— 链式
expect('hello').to.be.a('string');
expect(1 + 1).to.equal(2);
expect({ a: 1 }).to.deep.equal({ a: 1 });
// Jest expect —— 方法调用
expect('hello').toBeInstanceOf(String);
expect(1 + 1).toBe(2);
expect({ a: 1 }).toEqual({ a: 1 });
Jest expect 没有链式语言链(.to.be.a. 这种),每个断言是一个独立方法调用。风格差异主要是:
- Chai 读起来像自然语言句子
- Jest expect 读起来更像函数调用
否定、深度、包含
三个最常用的修饰器对比:
| 语义 | Chai expect | Jest expect |
|---|---|---|
| 严格相等 | .toEqual() | .toBe() |
| 深度相等 | .deep.equal() | .toEqual() |
| 否定 | .not.equal() | .not.toBe() |
| 包含元素 | .include() | .toContain() |
| 子集包含 | .include.members() | 无直接对应 |
| 键集合匹配 | .all.keys() | .toHaveProperty() 系列 |
Chai 在集合/对象维度更丰富(.members / .any.keys / .deep.property),Jest expect 以简洁直接为设计目标。
Jest expect 独有的能力
Jest expect 有一些 Chai 核心没有(或需要插件)的能力:
快照测试
// Jest expect —— 快照
expect(tree).toMatchSnapshot();
Chai 核心不支持快照(有第三方插件 chai-match-snapshot,但不如 Jest 内置的成熟)。
Mock 断言
// Jest expect —— 直接断言 mock 调用
expect(mockFn).toHaveBeenCalled();
expect(mockFn).toHaveBeenCalledWith('arg1', 'arg2');
expect(mockFn).toHaveBeenCalledTimes(3);
Chai 没有内置 mock,需要装 sinon-chai 插件(.called / .calledWith / .calledOnce)。
异步值
// Jest expect —— 原生支持 Promise
await expect(fetchData()).resolves.toBe('data');
await expect(fetchData()).rejects.toThrow();
Chai 需要 chai-as-promised 插件才能做到类似的 .eventually / .rejectedWith。
数组长度断言
// Jest expect —— 简洁
expect(items).toHaveLength(5);
// Chai
expect(items).to.have.lengthOf(5);
Jest 风格略短。
类型检查
// Jest expect —— 丰富的类型匹配器
expect(123).toBeGreaterThan(100);
expect(5).toBeLessThanOrEqual(10);
expect(value).toBeNumber(); // 非严格类型匹配
expect(value).toBeInstanceOf(Error);
Chai 独有的优势
三种风格任选
Chai 有 expect / should / assert 三种风格,Jest 只有 expect 风格。如果你或团队偏好 should 或 assert,Chai 更灵活。
框架无关
Chai 不绑定任何测试运行器。可以在 Mocha、Vitest、Karma、TAP、任何自定义运行器中使用。Jest expect 虽然可以脱离 Jest 使用,但它和 Jest 生态(mock、snapshot、coverage)的集成更紧密。
插件生态更丰富
Chai 有超过 200 个插件:chai-as-promised、chai-http、chai-dom、chai-subset、chai-bignumber…… 场景覆盖更广。Jest expect 的插件(extend-expect)数量少很多。
对象属性链访问
// Chai —— 原生支持属性路径
expect(user).to.have.deep.property('profile.settings.theme', 'dark');
// Jest expect —— 需要手动 get 或用 expect.objectContaining
expect(user.profile.settings.theme).toBe('dark');
// 或
expect(user).toEqual(expect.objectContaining({
profile: expect.objectContaining({
settings: expect.objectContaining({ theme: 'dark' })
})
}));
属性路径在嵌套对象测试中更简洁。
TDD assert 风格
// Chai assert 风格 —— Node assert 的超集
const assert = require('chai').assert;
assert.deepEqual(a, b);
assert.throw(fn, ErrorType, 'message');
Jest 没有 assert 风格接口,只有 expect 风格。
技术栈推荐
| 测试运行器 | 推荐断言库 | 理由 |
|---|---|---|
| Jest | jest expect | 原生集成最紧密,快照/mock 一站式 |
| Vitest | vitest expect(Jest 兼容 API) | 风格一致,Vite 生态 |
| Mocha | Chai | 最成熟、插件最多 |
| Ava | Ava 内置 assert(可用 Chai) | Ava 内置 assert 够用,要更多装 Chai |
| Karma | Chai | Karma 常与 Mocha + Chai 组合 |
| 自定义 | Chai | 框架无关 |
| Node 脚本(非正式) | Node 内置 assert(最简单) | 无需额外依赖 |
能否混用
能但不推荐——同一个测试文件里混用 Chai expect 和 Jest expect 会造成:
- 错误消息格式不统一
- 团队风格分裂
- 两个断言库增加 bundle size
项目内统一一种就好。
迁移场景
Jest → Chai(少见)
// Jest expect
expect(result).toEqual(expected);
expect(mockFn).toHaveBeenCalled();
// Chai(+ sinon-chai 插件)
expect(result).to.deep.equal(expected);
expect(mockFn).to.have.been.called();
Chai → Jest expect(Mocha 项目迁移到 Jest)
// Chai expect
expect({ a: 1 }).to.deep.equal({ a: 1 });
expect(arr).to.include.members([1, 2]);
// Jest expect
expect({ a: 1 }).toEqual({ a: 1 });
expect(arr).toEqual(expect.arrayContaining([1, 2]));
注意 .include.members 在 Jest expect 里用 expect.arrayContaining 替代。
最终建议
选哪个取决于你用的测试运行器:
- Jest / Vitest 用户 → 直接用内置 expect,省事省心
- Mocha / Karma / 其他运行器用户 → Chai,灵活且功能丰富
- Node 内置 assert 用户 → Chai assert 风格是零成本升级
项目内统一一种断言风格,不要混用。