Examples Become Tests
Requirements → examples → expected behaviour → tests. The concept record's tests each name the example they came from: "adding the same product twice increases the quantity" is add-again, encoded. A test that cannot name its example is testing the code's shape, not the cart's behaviour.
The situation, the reflex, and why it stalls
Every lesson starts where being stuck starts: someone has a problem, and the first move that comes to mind feels like progress.
The examples exist and the code works for them by hand. How do the examples become tests without the tests becoming a second implementation to maintain?
You have the before / after examples on a page and the cart in a file. Someone says "now write tests" and you open a test file and freeze: test what? You start writing a test that calls find on the items array to check it was called, and it feels wrong.
Test the implementation: assert that addItem calls find, that the array has the right length, that the function returns the same object. Coverage tools say 100% and the suite is green.
The suite tests that the code is shaped the way it is shaped. Switch the array for a map — same behaviour — and half the tests fail, so the tests are now the thing preventing the refactor they were supposed to permit.
- The suite tests that the code is shaped the way it is shaped. Switch the array for a map — same behaviour — and half the tests fail, so the tests are now the thing preventing the refactor they were supposed to permit.
- None of the tests would fail if the second add produced a duplicate, because none of them was written from the add-again example; they were written from the function's body, and the body has no duplicate in it to imagine.
- The tests cannot be read as a description of the cart. A stakeholder asking "what does it do when I add the same thing twice?" cannot find the answer in a test named
addItem calls find.
The move
Precisely enough to apply it to a problem you have never seen — not a slogan.
- Take each example and translate it directly: the before becomes the setup, the operation becomes the call, the after becomes the assertion. Nothing else goes in. The test asserts on the state — entries and quantities — and on nothing about how the state was produced.
- Name the test after the behaviour and record the example it came from. The concept record does this in data: each test has a
fromExamplefield. When a test fails, the example is the specification to compare against; when an example changes, the field says which test to change. - Invalid examples become two assertions, not one: the call is refused with a message naming the rule, and the state afterwards equals the state before. The second assertion is the one that catches a check placed after the mutation.
- Keep the same tests across representations. A test written from an example calls the operation and reads the state; it does not know whether the state is an array or a map. That is the check that the tests are about behaviour: the four-language implementations in the record share one set of tests.
From requirement to test, and what each step adds
The path has four stops and each one adds something the previous could not hold. A requirement says what the cart is for; an example makes it concrete; the expected behaviour generalises the example into a rule; the test makes the example executable. Skipping a stop is where the failures live — the reflex skips straight from requirement to test, and the test ends up describing the code.
- 1Requirement
"A shopper can add products to a cart." What the cart is for, in the words of whoever asked.
fails by Going straight to code — the requirement has no second add in it.
- 2Example
[Laptop × 1] + add Laptop → [Laptop × 2]. Concrete, with a hesitation resolved.
fails by Only the empty-cart example, so the branch never appears.
- 3Expected behaviour
"Adding a product already in the cart increases its quantity; one entry per product." The example, generalised into a rule.
fails by A rule with no example beside it — plausible, unverified.
- 4Test
Setup from the before, call from the operation, assertion from the after; named for the behaviour, tagged with the example.
fails by Assertions about internals — the test now describes the code and blocks the refactor.
The record's tests, each naming its example
The three tests below are the shopping-cart record's, in one language. Each carries the example id it came from; read the assertion and the example's after is visible in it. The second test is the one to study — it asserts the rejection and then asserts the state, because the invalid example's after is "unchanged", and that is a claim the test has to check.
- Example[Laptop × 1] + add Laptop → [Laptop × 2] — the after, decided in words.
- Rule"One entry per product" — the example generalised, the reason the find exists.
- OperationaddItem: catalog check, quantity check, find, branch, increase or append.
- TestTwo calls, one deepEqual on the items — the example made executable and named after it.
1// adding the same product twice increases the quantity (fromExample: add-again)2const cart = addItem(addItem(createCart(), 'laptop'), 'laptop')3assert.deepEqual(cart.items, [{ productId: 'laptop', quantity: 2 }])4 5// a zero quantity is rejected and the cart is unchanged (fromExample: invalid-zero)6const empty = createCart()7assert.throws(() => addItem(empty, 'laptop', 0), /positive/)8assert.deepEqual(empty.items, [])9 10// changing a quantity to zero removes the entry (fromExample: to-zero)11const two = addItem(createCart(), 'laptop', 2)12changeQuantity(two, 'laptop', 0)13assert.deepEqual(two.items, [])Nothing here mentions find, push, filter or an array. The same three assertions exist in Python and C++ in the record, and would pass against the map representation.
How far the tests go, and why each level exists
Examples produce the first rung; the rungs above it are where a test suite can go once the examples exist, and each has a reason. The ladder is not a checklist — a V0 cart stops at the second rung — but it says what the next level buys and why the record stops where it does.
- Example, on the pageBefore / operation / after, in words, with the hesitation resolved. — It is readable by whoever asked for the cart and is where disagreements about behaviour are settled.
- Example, as a testSetup, call, assertion on state; named for the behaviour; tagged with the example. — It runs on every change and fails when the behaviour changes — and only then.
- Invariant, as a propertyFor any sequence of adds, every quantity > 0 and no product id repeats — checked over generated inputs. — Examples check the cases you thought of; a property checks the rule on the ones you did not.
- Behaviour, as a characterisationA snapshot of what the existing cart does, taken before a refactor of code nobody fully understands. — When the examples were never written, the running code is the only specification there is — and this rung records it before changing it.
The implementation ladder
Concept, examples, pseudocode, code, tests, production — for the concept this lesson is about. Code is the fourth tab, not the first.
Shopping Cart = A temporary collection of products the user intends to purchase, held between browsing and checkout.
- Does a cart have identity? Yes, weakly. Two carts with the same items are still two carts, because each belongs to someone and will become a different order. It needs an id once it leaves memory; in memory the variable is the identity.
- Who owns it? A shopper — a logged-in user or an anonymous session. The owner is part of the state because "my cart" has to be findable again.
- How long does it exist? From the first add until checkout or abandonment. Whether it survives a reload, a closed browser or a login is not a property of the concept; it is a persistence decision made later, and each answer changes where the cart lives.
- Should it survive reload? Usually yes for a store, usually no for a demo. V1 in memory says no; V2 browser storage says yes on one device; V3 server storage says yes everywhere the user is logged in.
- Should it survive login? Only if an anonymous cart and a logged-in cart are merged — a rule that does not exist in V1 and appears as a modification later.
- itemscollection of CartItemkeepThe cart is its items; without them nothing else means anything.
- items[].productIdidkeepThe reference to what is being bought. The catalog owns the product; the cart only points at it.
- items[].quantityinteger > 0keepTwo laptops is one entry with quantity 2, not two entries — the rule "one entry per product" needs a quantity to hold.
- owneruser id or session iddependsSo the cart can be found again by the person it belongs to.
- items[].productNamestringderiveIt would be convenient to render the cart without a catalog lookup.
- items[].pricemoneydependsThe total needs a price per item.
- totalmoneydropEvery screen shows the total.
- currencycodedependsPrices need a currency to be added.
- createdAttimestampdropAbandoned carts might be expired or emailed about.
- update Add item — the updated cart
- delete Remove item — the updated cart
- update Change quantity — the updated cart
- read View items — the list of entries — product id and quantity — for rendering
- domain Calculate total — the sum of price × quantity over the entries
- delete Clear cart — the empty cart
- • Every quantity is greater than zero.
- • One logical entry per product.
- • The total is never negative.
- • An unknown product cannot be added.
- • Quantity cannot exceed available stock — if inventory is enforced here.
How to do it
Most important first.
- One test per example, in three parts: before as setup, operation as the call, after as the assertion on state (Invariants as Tests).
- Write
fromExamplebeside every test name, as the record does; a test with nothing to write there is testing shape. - For every invalid example, assert the rejection and then assert the state is unchanged.
- Run the tests against the other representation, or another language's implementation; a test that fails on a behaviour-preserving swap is coupled to the implementation (Array Cart vs Map Cart).
- Let the examples grow the tests, not the other way round: a new test starts as a new before / after on the page (Normal, Edge, Invalid).
Worked on a concrete problem
The move has to produce something. This is what it produced.
- add-again, from the record:
const cart = addItem(addItem(createCart(), 'laptop'), 'laptop'); assert.deepEqual(cart.items, [{ productId: 'laptop', quantity: 2 }]). Named "adding the same product twice increases the quantity". Setup is the before, two calls are the operation and its precondition, the assertion is the after — with one entry, which is what a duplicate would break. - invalid-zero:
assert.throws(() => addItem(cart, 'laptop', 0), /positive/); assert.deepEqual(cart.items, []). The second line is the example's after. A version that appended and then threw would pass the first line and fail the second. - to-zero: add Laptop × 2, change to 0, assert items is []. The same three tests exist in Python and C++ in the record, reading the same state; the array could be a map underneath and none of them would notice.
How you know it worked
What now exists that did not before, and what question you can now ask.
- Every test names the example it encodes, and every example on the page has a test — or a note saying why not.
- The suite reads as a description of the cart's behaviour, and a stakeholder could find "what happens when I add twice?" in it.
- Swapping the representation leaves the suite green.
The questions you can now ask
The field this whole domain exists for. After this lesson, these are the questions to put to an unfamiliar problem.
- ?Which example is this test — and if none, what is it testing?
- ?Does the assertion read the state, or the shape of the code that produced it?
- ?For each invalid example, is there an assertion that the state survived the rejection?
- ?Would this suite stay green if the array became a map?
What can go wrong
- Examples are translated and then "improved" with assertions about internals — that
findwas called, that the array is the same reference — and the coupling comes back. - Every example becomes a test, including the ones that exercise no new branch, and the suite grows until nobody reads it. The examples that produced code are the tests; the rest are documentation.
- The test is written from the code's current behaviour and back-labelled with an example, so the example inherits the bug.
- Behaviour-only tests say nothing about performance; the O(n) find and the O(1) map lookup pass the same suite, and a slow representation is invisible to it.
- One test per example is a small suite, and some teams want assertions on internals for diagnostic speed when a test fails — a legitimate want that couples the suite.
- Keeping
fromExampleaccurate is bookkeeping, and bookkeeping decays; a suite where the field is wrong is worse than one without it.
- "Tests are the specification." The examples are; the tests are the examples made executable. When they disagree, the example is where the argument happens, because it is readable by the person who asked for the cart.
- "100% coverage" means the behaviour is tested. Falsifiable: a suite asserting that
findwas called covers every line of addItem and never notices a duplicate entry. Coverage counts lines run, not examples checked. - "Write the tests first" contradicts this lesson. It does not — the before / after example *is* the test written first, in words; TDD writes it in code. The order examples → code → tests is the beginner's form of the same discipline.
Where this applies
Problem-solving advice is stated as universal far more often than it is. These labels say what each method is specific to — and where CONTESTED appears, the note gives the strongest form of the opposing view.
- GENERALBefore as setup, operation as call, after as assertion is the shape of a behavioural test for any concept, and the field naming the source example works for a queue or a login as it does for the cart.
- CONTESTEDA school of testing holds that unit tests should assert on interactions — that the collaborator was called with the right arguments — because it isolates each unit and pinpoints failures faster. Its strongest form is that behaviour-only tests over a whole concept fail far from the cause and let a broken collaborator hide behind a passing integration. The lesson's answer is that for a concept with in-memory state and no collaborators, the state is the behaviour and interaction assertions only couple the suite to the representation.
- ILLUSTRATIVEThe three tests quoted are the concept record's invented suite; a real cart would have more, and the products and quantities are placeholders.
Where the depth lives
This domain asks the question and hands the answer off by name.