JS Sketchbooksee JavaScript think ✏️
← back to phase 10

10.3 — Anatomy of a test

Before any framework, the demystification: a test is just a function that calls your function and checks the answer. Nothing more. Everything a test runner adds later — 10.5’s Vitest, Phase 11’s Playwright — is comfort around this one shape.

The shape has three beats and a famous name: Arrange, Act, Assert. Learn it here on five lines of code, and you’ll recognize it in every test file you read for the rest of your career.

watch it happen
// the machine under test
function withTax(price) {
  return price * 1.25;
}

// a TEST is just a function that checks it
function test_addsTax() {
  // Arrange — set the stage
  const price = 100;

  // Act — ONE call: the behavior
  const actual = withTax(price);

  // Assert — compare against expected
  const expected = 125;
  if (actual === expected) {
    console.log("PASS: adds 25% tax");
  } else {
    console.log("FAIL: got " + actual);
  }
}

test_addsTax();

Look at the whole pane and notice what ISN’T there: no framework, no import, no magic. test_addsTax is an ordinary function (Phase 3, verbatim) that calls withTax and checks the answer with an if (2.1). Whatever mystique “testing” had — that’s all of it.

one test, three beatsARRANGEconst price = 100ACTwithTax(price)ASSERTactual === expected?no framework, no magic: a test is a FUNCTION that calls your function andchecks the answer
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.”

The vocabulary you’ll meet in the wild: the function being tested is the SUT — system under test (here, withTax). Some teams write the three beats as Given–When–Then — identical anatomy, storytelling costume. Recognize both; they are the same three beats.

Why one call in Act, precisely: two acts in one test means the second runs against a world modified by the first — its failures become ambiguous (did act two break, or did act one leave a mess?). When a flow genuinely needs multiple steps, that’s an integration/E2E test doing flow-work on purpose — and even there, one flow per test.

The enshrined-bug trap from the fifth step deserves its full horror story: a function rounds wrong, a hurried tester copies its output into expected, and the suite goes green. From that day the suite actively DEFENDS the bug — anyone who fixes the rounding turns the suite red and gets told to “fix their mistake.” One copied expected value can outlive everyone who remembers why. Compute expected values from the spec. Always.

Job note: interviewers love handing you a function and saying “test this.” The professional move you now own: enumerate behaviors first (happy path, zero, negative, wrong type), then write one AAA test per behavior with sentence names. That enumeration step — before any code — is what separates testers from typists.

your turn

⌨️ your first two tests, by hand

No framework yet — build the whole idea from raw materials: a check helper and two one-behavior tests for withTax. (10.5 gives you the professional version of exactly this.)

requirements:

  • Write withTax(price) returning price * 1.25 — the machine under test.
  • Write check(name, actual, expected): print PASS: name when they match strictly, otherwise FAIL: name — got actual. One helper, reused by every test.
  • Test one — adds 25% tax: arrange a price of 100, act with ONE call, assert against an expected you compute on paper (not by running!).
  • Test two — zero price stays zero: the edge case gets its own test, its own sentence-name, its own verdict.

when you press RUN, the console must show exactly:

PASS: adds 25% tax
PASS: zero price stays zero

✏️ Quick check 1

The three beats of every test, in order — name them (comma-separated).

✏️ Quick check 2

Your expected value should come from the spec worked out on paper — or from running the code and copying its output. Which one?

✏️ Quick check 3

A single test asserts five different things and fails. Roughly how much does its failure tell you?

teach it back

🗣️ Now teach it back

Explain to a friend what a test literally is, walk the three beats with the withTax example, state where expected values must come from (and the horror story if not), and the one-behavior-per-test rule.

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
A test = a function that calls your function and checks. Three beats, one name: ARRANGE (build the inputs) → ACT (one call — the behavior) → ASSERT (actual === expected, verdict out loud).
Expected values come from the SPEC, on paper — never from the code’s own output. Copied output = today’s bug enshrined as law, defended by the suite forever.
One behavior per test (a failure should BE the diagnosis) · names are sentences (the report reads as a spec) · edges get their own tests · AAA scales unchanged to Playwright: navigate / click / expect.
next: 10.4 Assertions →