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.
// 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.
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 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)returningprice * 1.25— the machine under test. - Write
check(name, actual, expected): printPASS: namewhen they match strictly, otherwiseFAIL: name — got actual. One helper, reused by every test. - Test one —
adds 25% tax: arrange a price of100, 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:
✏️ 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?
🗣️ 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.