Unit test design
A unit test is documentation that runs: it names a rule, shows the inputs
that trigger it, and pins the result so a change cannot pass unnoticed. When
one test sets up state between assertions, calls the system three times, or
checks six unrelated fields, a failure points nowhere and the reader learns
nothing. Shape each test so the behavior it guards is visible at a glance.
Method
- Separate arrange, act, and assert with a blank line each. Arrange
constructs inputs and the object under test, act is the one call you are
exercising, assert pins the outcome. If the three blocks do not stand
apart on screen, the test is carrying more than one behavior.
- Make act a single invocation. The behavior under test is one call:
total = invoice.total(). Needing two calls to reach the assertion means
you are testing a sequence, so name it that way or split it in two.
- Assert one intent, not one line. A test called
applies_bulk_discount
checks the discount and nothing else. Compare a whole result object when
the fields express the same claim, and move unrelated claims, the
timestamp, the audit log, into their own tests.
- Keep computation out of the test. Write the literal
42, never
qty * price. A test that recomputes the expected value with the same
formula as the code passes whenever both share the same bug.
- Build inputs with named factories. A helper such as
an_order(status="shipped") states the field that matters and defaults
the rest, so the one variable under test is not buried in a valid object.
- Cover the behavior's edges as separate cases. For the rule at hand add
empty, single, many, boundary, and the failure path with an explicit
assertRaises or expect().toThrow, each as its own named test rather
than one parametrized block that reports a single pass.
Litmus tests
- From the test name and assert block alone, can you state the rule without
reading the arrange phase?
- On failure, does the message name one broken behavior instead of diffing a
twelve-field object?
- Could you delete the implementation and rebuild its spec from the test
names and assertions?
Boundaries
This governs the shape of one unit test, not what to test or at which layer:
defer scope to testing-strategy and names to test-naming. Where a project
fixes an assertion library or factory style, follow it over the exact forms
shown here.
1---2name: unit-test-design3description: Write unit tests that document behavior through explicit arrange-act-assert phases and a single assertion of intent. Use when writing or repairing a unit test that has grown hard to read.4---56# Unit test design78A unit test is documentation that runs: it names a rule, shows the inputs9that trigger it, and pins the result so a change cannot pass unnoticed. When10one test sets up state between assertions, calls the system three times, or11checks six unrelated fields, a failure points nowhere and the reader learns12nothing. Shape each test so the behavior it guards is visible at a glance.1314## Method15161. **Separate arrange, act, and assert with a blank line each.** Arrange17 constructs inputs and the object under test, act is the one call you are18 exercising, assert pins the outcome. If the three blocks do not stand19 apart on screen, the test is carrying more than one behavior.202. **Make act a single invocation.** The behavior under test is one call:21 `total = invoice.total()`. Needing two calls to reach the assertion means22 you are testing a sequence, so name it that way or split it in two.233. **Assert one intent, not one line.** A test called `applies_bulk_discount`24 checks the discount and nothing else. Compare a whole result object when25 the fields express the same claim, and move unrelated claims, the26 timestamp, the audit log, into their own tests.274. **Keep computation out of the test.** Write the literal `42`, never28 `qty * price`. A test that recomputes the expected value with the same29 formula as the code passes whenever both share the same bug.305. **Build inputs with named factories.** A helper such as31 `an_order(status="shipped")` states the field that matters and defaults32 the rest, so the one variable under test is not buried in a valid object.336. **Cover the behavior's edges as separate cases.** For the rule at hand add34 empty, single, many, boundary, and the failure path with an explicit35 `assertRaises` or `expect().toThrow`, each as its own named test rather36 than one parametrized block that reports a single pass.3738## Litmus tests3940- From the test name and assert block alone, can you state the rule without41 reading the arrange phase?42- On failure, does the message name one broken behavior instead of diffing a43 twelve-field object?44- Could you delete the implementation and rebuild its spec from the test45 names and assertions?4647## Boundaries4849This governs the shape of one unit test, not what to test or at which layer:50defer scope to testing-strategy and names to test-naming. Where a project51fixes an assertion library or factory style, follow it over the exact forms52shown here.