Table of Contents#
- Introduction
- Understanding the Problem: Why
includeMight Fail - Chai’s
includevs.deep.include: What’s the Difference? - Using
deep.includefor Nested Objects - Advanced Scenarios: Partial Matches and Ignoring Extra Properties
- Troubleshooting Common Issues
- Best Practices for Testing Object Inclusion
- Conclusion
- References
Understanding the Problem: Why include Might Fail#
To understand why include fails for objects, let’s start with how Chai’s include assertion works by default (without the deep flag).
How include Works for Primitives vs. Objects#
Chai’s include assertion checks if a value "includes" another value. For primitives (strings, numbers, arrays of primitives), this works as expected:
// Primitives: include works!
expect([1, 2, 3]).to.include(2); // Passes (array includes 2)
expect("hello").to.include("ell"); // Passes (string includes "ell")
expect({ a: 1, b: 2 }).to.include({ a: 1 }); // Fails! (Why?)The last test fails because objects are reference types in JavaScript. Two objects with identical properties are not considered equal by default—JavaScript compares their memory references, not their content. For example:
const obj1 = { a: 1 };
const obj2 = { a: 1 };
console.log(obj1 === obj2); // false (different references)When you use expect(parent).to.include(child) with objects, Chai uses strict equality (===) to check if child is a direct property of parent. Since child is a separate object (different reference), the check fails—even if their properties match.
Example: The Failing Test#
Let’s reproduce the problem with a real-world example. Suppose we have a user object and want to verify it contains a profile sub-object:
const user = {
name: "Alice",
profile: { age: 30, city: "Paris" } // Parent object
};
const expectedProfile = { age: 30, city: "Paris" }; // Child object to check
// Test: Does user include expectedProfile?
expect(user).to.include({ profile: expectedProfile }); // Fails!Why does this fail? Because user.profile and expectedProfile are two distinct objects with different references. Chai’s include (shallow check) compares them strictly, so the test fails.
Chai’s include vs. deep.include: What’s the Difference?#
To solve the object inclusion problem, Chai provides the deep flag. Let’s clarify the difference:
Shallow include (Default)#
- Uses strict equality (
===) for property values. - Works for primitives (strings, numbers, booleans) and references.
- Fails for objects/arrays with identical content but different references.
Deep include (deep.include)#
- Uses deep equality to compare property values recursively.
- Checks if all properties of the "child" object exist in the "parent" object (and nested properties match).
- Ignores object references and focuses on content.
Key Takeaway#
Use deep.include when testing if an object contains another object (or nested objects).
Using deep.include for Nested Objects#
Let’s fix the earlier example with deep.include. The deep flag tells Chai to recursively compare nested properties instead of checking references.
Step 1: Basic Object Inclusion#
const user = {
name: "Alice",
profile: { age: 30, city: "Paris" }
};
const expectedProfile = { age: 30, city: "Paris" };
// Use deep.include to check nested object content
expect(user).to.deep.include({ profile: expectedProfile }); // Passes!Now the test passes because deep.include recursively checks that user.profile has all properties of expectedProfile (age: 30, city: "Paris") with matching values.
Step 2: Partial Matches (Parent Has Extra Properties)#
deep.include checks for partial inclusion—the parent object can have extra properties not in the child. For example:
const user = {
name: "Alice",
profile: { age: 30, city: "Paris", country: "France" } // Extra property: country
};
const expectedProfile = { age: 30, city: "Paris" }; // No country
// Still passes: user.profile contains all properties of expectedProfile
expect(user).to.deep.include({ profile: expectedProfile }); // Passes!This works because deep.include only verifies that the child’s properties exist in the parent (and match), ignoring extra properties in the parent.
Step 3: Nested Arrays#
deep.include also works for nested arrays, as long as the array content matches:
const order = {
id: 123,
items: ["apple", "banana"] // Nested array
};
const expectedItems = ["apple", "banana"];
expect(order).to.deep.include({ items: expectedItems }); // Passes!If the array order matters, deep.include will enforce it. For unordered arrays, use Chai’s members assertion instead (e.g., expect(order.items).to.include.members(["banana", "apple"]).
Advanced Scenarios: Partial Matches and Ignoring Extra Properties#
While deep.include works for most cases, there are scenarios where you need more control—e.g., ignoring extra properties in nested objects or checking subsets of arrays. For these, the chai-subset plugin is invaluable.
What is chai-subset?#
chai-subset is a Chai plugin that extends assertions to check if an object is a subset of another, ignoring extra properties. It’s ideal for complex partial matches.
Step 1: Install chai-subset#
First, install the plugin via npm:
npm install chai-subset --save-devStep 2: Use includeSubset for Flexible Matching#
Require chai-subset in your test file and use to.includeSubset to check for subsets:
const chai = require("chai");
const chaiSubset = require("chai-subset");
chai.use(chaiSubset); // Enable the plugin
const expect = chai.expect;
const product = {
name: "Laptop",
specs: {
ram: "16GB",
storage: "512GB",
color: "silver" // Extra property we want to ignore
}
};
const expectedSpecs = { ram: "16GB", storage: "512GB" }; // Subset to check
// Test: product.specs should include expectedSpecs (ignore color)
expect(product.specs).to.includeSubset(expectedSpecs); // Passes!includeSubset recursively checks that all properties in expectedSpecs exist in product.specs, ignoring extra properties like color.
Step 3: Nested Subsets with Arrays#
chai-subset also handles nested arrays, even if the parent array has extra elements:
const data = {
users: [
{ name: "Bob", age: 25 },
{ name: "Charlie", age: 30 } // Extra user
]
};
const expectedUsers = [{ name: "Bob", age: 25 }]; // Subset of users
expect(data).to.includeSubset({ users: expectedUsers }); // Passes!Here, data.users contains the expectedUsers array as a subset, so the test passes.
Troubleshooting Common Issues#
Even with deep.include or chai-subset, tests may fail due to subtle issues. Let’s troubleshoot the most common ones:
1. Typos in Property Names#
A typo in a property name will cause the test to fail. Double-check spelling:
const user = { profile: { city: "London" } };
expect(user).to.deep.include({ profile: { cty: "London" } }); // Fails (typo: "cty" vs "city")2. Mismatched Data Types#
Chai’s deep check enforces strict data types. For example, 30 (number) vs. "30" (string) will fail:
const user = { profile: { age: "30" } }; // age is a string
const expected = { profile: { age: 30 } }; // age is a number
expect(user).to.deep.include(expected); // Fails (type mismatch)3. Circular References#
Objects with circular references (e.g., obj.self = obj) will cause deep.include to throw an error. Use util.inspect to debug or avoid circular structures in tests.
4. Arrays: Order vs. Subset#
deep.include checks array order strictly. If you need to ignore order, combine deep.include with members:
const basket = { fruits: ["orange", "apple"] };
const expected = { fruits: ["apple", "orange"] };
// Fails: order matters for deep.include
expect(basket).to.deep.include(expected);
// Passes: check if fruits include all expected elements (order ignored)
expect(basket.fruits).to.include.members(expected.fruits); Best Practices for Testing Object Inclusion#
To write reliable object inclusion tests, follow these practices:
1. Prefer deep.include for Simple Cases#
Use expect(parent).to.deep.include(child) for most object inclusion tests—it’s built into Chai and requires no plugins.
2. Use chai-subset for Complex Subsets#
For nested objects with extra properties or array subsets, chai-subset (via includeSubset) is more flexible than deep.include.
3. Test Explicitly#
Avoid over-specifying tests. Only check the properties you care about, not the entire object. This makes tests resilient to unrelated changes.
4. Document Test Intentions#
Write clear test descriptions to explain why the inclusion matters (e.g., it("should include user profile with age and city")).
5. Handle Edge Cases#
Test edge cases like empty objects, null/undefined values, and nested arrays to ensure robustness:
// Test empty object inclusion
expect({}).to.deep.include({}); // Passes (empty object includes empty object)
// Test null values
expect({ config: null }).to.deep.include({ config: null }); // PassesConclusion#
Testing if an object contains another object in Chai.js is straightforward once you understand the tools:
- Shallow
includefails for objects because it uses strict reference equality. deep.includefixes this by recursively comparing nested properties (use for most object inclusion tests).chai-subset(viaincludeSubset) handles advanced scenarios like ignoring extra properties or array subsets.
By applying these techniques, you can write reliable, readable tests that validate object structure with confidence.