Skip to content

Unit testing with features and testable properties

Unit tests specify how a unit works and verify it behaves correctly. Good tests enable us to modify and extend the system with confidence.

We write unit tests organized by features and testable properties, since this provides a compact, domain-agnostic way to achieve both goals.

Why this works

  • Features make tests navigable and purposeful.
  • Properties turn vague behavior into crisp, verifiable contracts.
  • The test suite becomes living documentation: feature sections describe what the unit offers, and property-labeled assertions show the expected behavior in concrete, runnable form.
  • Tests explain themselves, survive refactors, and catch regressions that matter.

For more background see Structure and Interpretation of Test Cases - Kevlin Henney

1. Identify the feature set

  • Define capabilities the unit provides that consumers can rely on, not internals.
  • Aim for orthogonality or layering
    • Orthogonality: features don't overlap; each serves a distinct purpose and can be tested independently.
    • Layering: higher-level features build on lower-level ones; tests of higher features rely on lower-level behavior already being tested.
  • Name features with short, proper English headings.

2. Identify testable properties for each feature

  • List general rules the feature must always satisfy (behavioral invariants).
  • Write properties as full English sentences; avoid overly short phrasing that obscures the behavior.
  • Use general wording when describing properties, not specific examples.
  • Validate each property with minimal, deterministic cases (happy path + edges).
  • Include negative properties (error conditions, invalid inputs).
  • Keep properties orthogonal: one behavior per test; avoid overlap and hidden coupling.

3. Implement the test

  • Identify features & testable properties before writing the test code.
  • The optimal test case is short with 3 parts: a) Arrange: prepare the unit b) Act: execute an action c) Assert: check the expected outcome.
  • Make features and properties easy to spot and change.
    • Use clear section markers (headings or comments) for each feature. In Java use nested classes or separate files.
    • Make the testable property clearly visible next to the case that exercises it.
    • Order features from foundational to advanced, and properties from simple/positive to complex/edge cases.
  • Maximize locality
    • Keep Arrange–Act–Assert together.
    • Data that influence Assert should be clearly visible in Arrange or Act.
  • Prefer small, representative, deterministic inputs; control nondeterminism (time, randomness, concurrency).
  • Keep shared setup/teardown simple; keep tests fast; minimize the number of different setup-states that different test-cases start from.
  • Use the terminology of the unit under test in the test code and the comments.
  • Prefer literal values (both inputs and expected outputs). Avoid computing data inline; it obscures results and risks duplicating logic under test. Literals keep outcomes clear and tests readable.

4. Coverage and Mutation testing

Our pipeline enforces 100% coverage and performs mutation testing of the code.

The feedback from the pipeline on your unit tests should primarily be considered as hints about features/testable properties that you have missed in your test design.

Test Doubles

A Test Double is any kind of pretend object used in place of a real object for testing purposes.

When using test doubles their name should be suffixed with the most descriptive name from the list below, i.e. TransactionContextFake.

The following kinds of test doubles exist:

  • Dummy objects are passed around but never actually used. Usually they are just used to fill parameter lists.
  • Fake objects actually have working implementations, but usually take some shortcut which makes them not suitable for production (an in memory database is a good example).
  • Stubs provide canned answers to calls made during the test, usually not responding at all to anything outside what's programmed in for the test.
  • Spies are stubs that also record some information based on how they were called. One form of this might be an email service that records how many messages it was sent.
  • Mocks are objects pre-programmed with expectations which form a specification of the calls they are expected to receive.

For more info see Mocks Aren't Stubs - Martin Fowler

Examples