JS Sketchbooksee JavaScript think ✏️
← back to phase 10

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.

watch it happen
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 sentence shapeexpect(actual)what the code produced.toBe / .toEqual / …the matcher: which QUESTION(expected)from the spec (10.3!)the grammar reads as a sentence: expect(actual).toBe(expected) — “I expectthe answer to be 125”
under the hood

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.

your turn

⌨️ 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: a and b, both { total: 125 } — two separate literals.
  • Print three verdicts, labeled exactly: toBe twins: false, toEqual twins: true, toBe same box: true (that last one compares a with a).

when you press RUN, the console must show exactly:

toBe twins: false
toEqual twins: true
toBe same box: true

✏️ 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.

teach it back

🗣️ 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.

a few sentences, minimum — you’ve got this
to remember
Grammar: expect(actual).matcher(expected) — reads as a sentence; expected still comes from the spec. toBe = strict === (value for primitives, ADDRESS for objects — 4.6).
The twin trap: two identical literals = two heap boxes → toBe fails; toEqual unpacks and compares structure (every layer — 4.7). Rule: toBe for primitives/identity, toEqual for data.
Family: toContain, toHaveLength, toBeCloseTo (floats, always — 1.9 settled), .not flips anything. Distrust toBeTruthy (passes everything non-falsy). Async/web-first assertions grow from this grammar in Phase 11.
next: 10.5 Vitest hands-on →