Skip to content

Characteristics of Good Tests

A test suite is an asset while it earns more than it costs, and a liability the moment it does not. These are the properties that decide which one you have.

graph TD
    G[Good test] --> C1[Correct: fails when behaviour is wrong]
    G --> C2[Precise: names one reason for failure]
    G --> C3[Fast: cheap enough to run constantly]
    G --> C4[Isolated: no dependence on other tests]
    G --> C5[Deterministic: same input, same verdict]
    G --> C6[Readable: states intent without explanation]
    G --> C7[Maintainable: survives refactoring]

The compact version of the same list is the FIRST principles.

The four questions a test must survive

Question If the answer is no
Would it fail if the behaviour were wrong? It is decoration. See mutation testing
Would it stay green through a refactor? It tests structure, not behaviour, and will be deleted eventually
Does a failure tell you what broke? Every failure becomes an investigation
Can a stranger read it in thirty seconds? Nobody will maintain it

The first two together are the whole game: a test must be sensitive to behaviour change and insensitive to structure change. Most bad tests fail one of those and usually fail the same one, by asserting on how the code works rather than what it produces.

Readable tests

def test_rejects_expired_card():
    card = a_card(expiry="2020-01")
    result = checkout.pay(cart_of(50), card)
    assert result.declined_because == "card_expired"
Practice Effect
Name after the behaviour The failure line already explains the problem
Visible arrange, act, assert The scenario reads top to bottom
Builders with defaults Only the relevant values appear
No logic in the test An if in a test means two tests
One reason to fail Nothing hides behind a first failed assertion

A test needing a comment to explain what it checks is a test that should be renamed.

Sensitivity, in both directions

graph LR
    B[Behaviour changes] --> T1{Test fails?}
    T1 -->|yes| GOOD1[Correct sensitivity]
    T1 -->|no| WEAK[Weak oracle, worthless test]
    S[Structure changes, behaviour same] --> T2{Test fails?}
    T2 -->|no| GOOD2[Correct insensitivity]
    T2 -->|yes| BRITTLE[Brittle test, refactoring tax]

Practically: assert on outputs and observable effects, use real collaborators where they are fast, and reserve interaction verification for cases where the interaction genuinely is the requirement.

Suite-level properties

Good individual tests do not guarantee a good suite.

  • Fast enough to run constantly. Wall clock time decides whether it is run at all.
  • No redundancy. Many old tests re-check the same rule through different paths, and deleting them costs nothing.
  • Parallel safe, which requires isolation.
  • Zero tolerated flakiness. One flaky test trains everyone to re-run until green.
  • Owned like production code. Reviewed, refactored, deleted when obsolete.

Check Your Understanding

#

Which pair of properties best defines a good test?

#

A test needs a comment explaining what it verifies. What does that indicate?