Chai 中文文档 下载 App

.deep 深度比较修饰器 - 让对象值相等不再因引用不同而失败

问题背景

JavaScript 的严格相等(===)对对象和数组比较的是引用,不是值:

const a = { x: 1 };
const b = { x: 1 };

a === b;       // false!不同引用
a == b;        // false
Object.is(a, b);  // false

这意味着 Chai 的默认 .equal(使用 ===)也会失败:

expect(a).to.equal(b);
// AssertionError: expected { x: 1 } to equal { x: 1 }

但从测试角度,通常关心的是"值是否相等",不是"是不是同一个引用"。这就是 .deep 的用武之地。

基本用法

在链式中插入 .deep,它之后的 .equal、.include、.members、.keys、.property 会改用深度相等:

expect({ a: 1 }).to.deep.equal({ a: 1 });   // 通过
expect([{ a: 1 }]).to.deep.include({ a: 1 });  // 通过
expect({ x: { a: 1 } }).to.deep.include({ x: { a: 1 } });  // 通过

.deep 可以和 .not 组合:

expect({ a: 1 }).to.not.deep.equal({ a: 2 });

修饰器顺序不影响结果:

expect({ a: 1 }).to.deep.not.equal({ a: 2 });
expect({ a: 1 }).to.not.deep.equal({ a: 2 });
// 两者等价

影响范围

.deep 只影响紧跟在它之后的特定断言方法。下表说明哪些方法受 .deep 修饰,哪些不受:

断言方法受 .deep 影响
.equal✅ 改用 deepEqual
.include / .contains✅ 深度匹配
.members / .sameMembers✅ 深度匹配
.keys / .have.keys✅ Set / Map 深度匹配
.property✅ 嵌套属性深度匹配
.propertyVal✅ 同上
.lengthOf❌ 不受影响(比的是数字)
.above / .below / .within❌ 不受影响
.match❌ 不受影响
.a / .an / .typeOf❌ 不受影响
.instanceOf❌ 不受影响
.throw❌ 不受影响

深度相等算法

Chai 使用独立的 deep-eql 库来执行深度相等判断。核心规则:

  1. 原始值(string、number、boolean、null、undefined):用严格相等 ===。
  2. 对象:递归比较所有自有可枚举属性的键和值。
  3. 数组:按索引递归比较每个元素。
  4. Date 对象:比较 .getTime() 返回的时间戳。
  5. RegExp 对象:比较 .source 和 .flags。
  6. Buffer:比较字节内容。
  7. Map:比较键值对集合(深度)。
  8. Set:比较成员集合(深度)。
  9. 循环引用:deep-eql 能处理循环引用而不会爆栈。

嵌套 .property 与 .deep

.deep.property 可以沿路径访问嵌套属性并做深度匹配:

const obj = { user: { profile: { name: 'Alice', age: 30 } } };

// 嵌套路径访问 + 深度比较值
expect(obj).to.have.deep.property('user.profile', { name: 'Alice', age: 30 });

// 多层路径也可以
expect(obj).to.have.deep.property('user.profile.name', 'Alice');

路径支持点号分隔:

expect({ a: { b: { c: 1 } } }).to.have.deep.property('a.b.c', 1);

assert 风格中的深度比较

assert 风格没有链式修饰器 .deep,而是显式调用独立方法:

const assert = require('chai').assert;

// 严格相等
assert.equal({ a: 1 }, { a: 1 });  // 失败

// 深度相等
assert.deepEqual({ a: 1 }, { a: 1 });  // 通过

对应的还有:

Set / Map 的深度比较

const set1 = new Set([{ a: 1 }, { b: 2 }]);
const set2 = new Set([{ a: 1 }, { b: 2 }]);

// Set 中对象是不同引用,但 .deep.keys 能处理
expect(set1).to.have.deep.keys([{ a: 1 }, { b: 2 }]);

部分匹配 vs 完全匹配

.deep.equal 做完全匹配——两个对象必须所有属性都相等:

expect({ a: 1, b: 2 }).to.deep.equal({ a: 1 });  // 失败——缺少 b

如果只需要验证对象的一部分属性,用 .deep.include:

expect({ a: 1, b: 2 }).to.deep.include({ a: 1 });  // 通过

这在测试中非常常用——API 返回的对象有很多字段,但你只关心其中几个。

性能注意

深度相等比严格相等慢,尤其是嵌套很深的大对象。但对于测试代码来说这通常不是瓶颈——测试不是高性能计算。

如果遇到超大型对象的深度相等测试超时,考虑:

常见误用

忘了加 .deep 导致断言失败

这是新手最常见的坑。你看到错误信息"expected { x: 1 } to equal { x: 1 }"会很困惑——明明值一样为什么不等?加 .deep 就好:

// ❌ 忘记加 .deep
expect(response.body).to.equal(expectedBody);

// ✅ 加上
expect(response.body).to.deep.equal(expectedBody);

加了 .deep 但实际不需要

// 不需要 .deep——比的是数字
expect(arr.length).to.deep.equal(3);  // 能用但多余

不会出错,但没必要。对原始值来说 .deep 不做任何额外事情。

混淆 .deep.include 和 .include

// expect 风格
expect({ a: 1, b: 2 }).to.deep.include({ a: 1 });  // 对象部分匹配
expect([1, 2, 3]).to.include(2);                    // 数组元素包含

// 不要在 .include 里放对象但忘记 .deep
expect([{ a: 1 }, { b: 2 }]).to.include({ a: 1 });  // 失败
expect([{ a: 1 }, { b: 2 }]).to.deep.include({ a: 1 });  // 通过

下一步