10.4 — Assertions
The assert beat from 10.3 gets its professional vocabulary today — and hiding inside it is the single most-asked beginner question in testing: why does expect(a).toBe(b) FAIL when a and b are identical objects?
You already own the answer — you learned it in 4.6, watching arrows point into the heap. Today it pays off, along with the whole matcher family and the skill of reading a failure message like a letter addressed to you.
import { expect, test } from "vitest";
test("primitives compare by value", () => {
expect(100 * 1.25).toBe(125);
});
test("objects compare by ADDRESS", () => {
const a = { total: 125 };
const b = { total: 125 };
expect(a).toBe(b); // ✗ FAILS!
expect(a).toEqual(b); // ✓ passes
});
expect(["a", "b"]).toContain("b");
expect("Playwright").toHaveLength(10);
expect(0.1 + 0.2).toBeCloseTo(0.3);
expect(125).not.toBe(126);First the grammar, because it’s designed to be read aloud: expect(actual).toBe(expected) — “I expect the answer to be 125.” actual is what the code produced (10.3’s Act); the matcher is which QUESTION you’re asking; expected comes from the spec (10.3’s soul-rule, still in force).
The deeper story, with the real names for things — this part is what turns “I saw it” into “I can explain it.”
Precisely what toBe uses: Object.is — which is === with two exotic exceptions (NaN matches NaN, and +0/-0 differ). You can go years without meeting either; “toBe is strict ===” is the working truth.
toEqual’s full contract: it walks both structures recursively, comparing primitives strictly at the leaves — and it treats undefined properties as ignorable ({ a: 1, b: undefined } equals { a: 1 }). Its stricter sibling toStrictEqual counts those too. If a suite you join uses the strict one everywhere, that’s why.
Why your exercise ban on JSON.stringify comparison matters beyond principle: stringify is order-sensitive ({ a, b } vs { b, a } serialize differently but ARE structurally equal), silently drops undefined and functions (4.13’s translation losses), and explodes on circular references. Real toEqual walks the tree precisely so it doesn’t inherit those lies.
Job note: failure messages are the product you ship to your future self. A precise matcher turns 2am debugging into reading: toHaveLength tells you the actual length, toContain shows the list. Choosing matchers IS writing documentation for the night something breaks.
⌨️ build toBe and toEqual yourself
The two most important matchers are shallow tools you already own. Build both, then prove the twin trap on purpose — once you’ve written toEqual, you’ll never confuse the two again.
requirements:
- Write
toBe(a, b): strict comparison, one line — exactly what the real matcher does. - Write
toEqual(a, b)for flat objects: the key lists must be the same length (4.8), and EVERY key’s value must match strictly (4.10 gave you the one-word tool for “all of them pass”). - Arrange the twins:
aandb, both{ total: 125 }— two separate literals. - Print three verdicts, labeled exactly:
toBe twins: false,toEqual twins: true,toBe same box: true(that last one comparesawitha).
when you press RUN, the console must show exactly:
✏️ Quick check 1
const a = { total: 125 }; const b = { total: 125 }; — does expect(a).toBe(b) pass or fail?
✏️ Quick check 2
You’re asserting that 0.1 + 0.2 equals 0.3. Which matcher?
✏️ Quick check 3
items.length is 999 but you expected 3. Does expect(items.length).toBeTruthy() catch the bug? Type yes or no.
🗣️ Now teach it back
Explain the twin trap to a friend: why toBe fails on two identical-looking objects (use 4.6’s picture), what toEqual does differently, and the rule for choosing between them — plus which matcher float math always gets.
Write it as if your friend is sitting next to you. Saved to your journal — future-you will use these notes to teach others.