.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 库来执行深度相等判断。核心规则:
- 原始值(string、number、boolean、null、undefined):用严格相等
===。 - 对象:递归比较所有自有可枚举属性的键和值。
- 数组:按索引递归比较每个元素。
- Date 对象:比较
.getTime()返回的时间戳。 - RegExp 对象:比较
.source和.flags。 - Buffer:比较字节内容。
- Map:比较键值对集合(深度)。
- Set:比较成员集合(深度)。
- 循环引用: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 }); // 通过
对应的还有:
assert.deepEqual(a, b)assert.notDeepEqual(a, b)assert.sameDeepMembers(set1, set2)assert.includeDeepMembers(needle, haystack)assert.deepProperty(obj, path)assert.deepPropertyVal(obj, path, expected)assert.deepNestedPropertyVal(obj, path, expected)
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 返回的对象有很多字段,但你只关心其中几个。
性能注意
深度相等比严格相等慢,尤其是嵌套很深的大对象。但对于测试代码来说这通常不是瓶颈——测试不是高性能计算。
如果遇到超大型对象的深度相等测试超时,考虑:
- 改用
.property逐个验证关键字段。 - 把对象精简后再断言。
- 检查是否有循环引用被误处理。
常见误用
忘了加 .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 }); // 通过
下一步
.any/.all集合逻辑 → .any 和 .all- 完整修饰器列表 → 语言链总览
- 对象属性相关断言 → 对象属性断言