ConstructiCat Logo
CodeBust.
Browse section ▾

Sensitive Equality.

A test verifies behavior by comparing an object's string representation (toString(), JSON.stringify(), rendered HTML) against an expected string literal, coupling the test to incidental formatting rather than the values that actually matter.

##Signs and Symptoms

You recognize Sensitive Equality when an assertion's actual side serializes an object to text and its expected side is a string literal that bundles many fields together with separators, quotes, brackets, and whitespace.

Tell-tale signs:

  • The actual value comes from .toString(), String(x), a template literal, JSON.stringify(...), wrapper.html(), el.outerHTML, or a snapshot of a serialized blob.
  • The expected value is a long literal string (often pasted from a previous failing run) and you can't tell at a glance which part of it is the thing under test.
  • The test breaks on changes that don't affect behavior: reordered keys, added/removed whitespace, number precision, date format, locale, timezone, or Map/Set/object iteration order.
// Comparing a serialized form to a string literal
expect(JSON.stringify(user)).toBe('{"name":"Bob","age":30}');

// Asserting on toString() output
expect(order.toString()).toBe('Order[id=7, total=42.00, items=2]');

// Diffing full rendered HTML against a literal
expect(wrapper.html()).toBe('<div class="card"><h2>Bob</h2><span>30</span></div>');

// Locale/timezone-sensitive string comparison
expect(d.toString()).toBe('Mon Jun 15 2026 00:00:00 GMT+0000 (UTC)');

##Reasons for the Problem

Why it happens

  • It's fast and easy. As van Deursen et al. note in Refactoring Test Code, it is quick to compute an actual result, map it to a string, and compare it to a string literal representing the expected value — often the literal is just copied from a failing run. Snapshot tooling makes this even more tempting.
  • The object has no meaningful equality (equals/deep matcher) wired up, or the object graph is large, so dumping it to a string feels simpler than asserting field by field.

Why it hurts

  • Fragility / false failures. The string carries many irrelevant details — commas, quotes, spaces, key ordering, float precision, locale, timezone, collection iteration order. Whenever the toString()/serialization format changes, unrelated tests start failing even though the behavior is correct. This is a classic cause of Fragile Test.
  • Maintainability. A single formatting or library change forces edits across many tests, and the giant expected literal is tedious to update and easy to get subtly wrong.
  • Readability / obscured intent. A wall-of-text expected value hides what is actually being verified; a reviewer can't see the one field the test cares about.
  • False confidence. A stringified blob can pass for the wrong reasons (two distinct states that serialize to the same text), and it nudges developers to re-record the literal/snapshot on failure without checking the new value is actually correct.
  • Flakiness. Locale-, timezone-, and ordering-sensitive strings make outcomes depend on the environment the test runs in.

##Treatment

Assert on the data, not on its rendering.

  1. Compare structured values with deep equality instead of string equality (toEqual/toStrictEqual), which is order- and whitespace-independent for objects.
  2. Verify only the relevant attributes with partial matchers (toMatchObject, expect.objectContaining) so a formatting change elsewhere can't break the test.
  3. Parse, don't string-match. When you receive a serialized payload (e.g., an API JSON body), parse it back into a structure and compare structures.
  4. Introduce an equality/comparison method on the object (the refactoring van Deursen et al. recommend) and assert object equality rather than toString() equality.
  5. If you must compare serialized output, canonicalize first — sort keys, pin locale/timezone, fix numeric precision — and prefer a reviewed snapshot over a hand-pasted inline literal.
  6. For DOM/component tests, query semantically (Testing Library getByRole, toHaveTextContent) instead of diffing full HTML.
// Before — Sensitive Equality
expect(JSON.stringify(user)).toBe('{"name":"Bob","age":30}');

// After — structural equality (key order / whitespace independent)
expect(user).toEqual({ name: 'Bob', age: 30 });

// After — assert only the field under test
expect(user).toMatchObject({ age: 30 });
// Before — string-matching an API payload
expect(res.text).toBe('{"id":7,"total":42}');

// After — compare parsed structures
expect(JSON.parse(res.text)).toEqual({ id: 7, total: 42 });