HN Debrief

Composable Tests

  • Programming
  • Developer Tools
  • Testing
  • Software Engineering

Kent Beck’s post uses tiny examples to argue that tests often contain accidental duplication. The core move is to separate “what must be true before this check” from “what this test is actually trying to prove,” then compose those pieces instead of reasserting earlier behavior in every later test. That sounds simple in toy code, but the reaction landed on a harder question: are you composing tests, or just coupling them.

If your suite is slow or repetitive, attack duplication with fixtures, helpers, and explicit setup structure before you let tests depend on other tests. Also add randomized or reversed execution to CI, because order-sensitive failures are often the fastest way to discover that your tests or code are leaking state.

Discussion mood

Mostly skeptical but pragmatic. People respected the goal of reducing repetitive setup, but many felt Beck’s example blurred the line between reusing setup and coupling tests. The warmest agreement centered on randomizing order, parallel execution, and other techniques that expose hidden shared state.

Key insights

  1. 01

    Adversarial test ordering finds real bugs

    Running a supposedly passing suite repeatedly in random order, reverse order, or with asynchronous execution is a cheap way to expose state leakage that isolated happy-path runs miss. The value here is not philosophical purity. It is that order-dependent failures usually mean your code or fixtures are carrying hidden state, and every such failure is a real defect rather than a flaky curiosity.

    Add randomized and reversed execution modes to CI, and keep at least one stress job that reruns green tests under shuffled order. If you use a runner like AVA, treat its async defaults as a diagnostic tool, not just a performance feature.

      Attribution:
    • pfdietz #1 #2
    • natbennett #1
    • neuronexmachina #1
    • sholladay #1
  2. 02

    Workflow tests justify some coupling

    Long UI and database-heavy scenarios change the economics. When the first part of a workflow is expensive, redoing it in every test burns time without adding much confidence, so stepwise execution and declared dependencies can be a reasonable compromise. The catch is that this only works when the shared part is really setup. As soon as later checks need data or behavior produced by earlier checks, you are no longer just reusing structure. You are creating a state-passing protocol that most frameworks handle poorly.

    Reserve stepwise or dependency-based tests for slow end-to-end paths where repeated setup dominates cost. Keep the dependency boundary narrow and explicit, and avoid using this pattern where later tests need opaque state from earlier ones.

      Attribution:
    • avensec #1
    • RHSeeger #1
    • brabel #1
    • troupo #1
    • drdexebtjl #1
  3. 03

    Frameworks do not guarantee isolation

    Setup hooks and sequential execution can make interference less visible, but they do not solve it. Globals, caches, databases, reused threads, and ThreadLocal state can still bleed across tests, especially once parallel execution is enabled. Calling a suite “isolated” because the framework has setUp and tearDown misses the point. Isolation lives in how each test constrains state and data boundaries.

    Audit your suite for state that outlives a single test, including thread-local values and shared service data. Before adding any composition pattern, decide exactly which dimensions are isolated and which are intentionally shared.

      Attribution:
    • solarengineer #1
    • aleksiy123 #1 #2
    • locknitpicker #1
  4. 04

    Testing ROI often starts in the middle

    Several comments converged on a more useful test portfolio than the article itself discussed. Acceptance tests define the user-visible workflow, integration tests cover the key seams inside that workflow, and unit tests are added selectively where the logic is risky or cheap to check. That approach rejects both extremes. You do not need every tiny intermediate test, and you should not assume one giant end-to-end scenario replaces all structure.

    Review your suite by cost and defect yield, not by test ideology. If you have lots of unit tests with low signal or a few expensive end-to-end tests with poor diagnosis, fill in the middle layer first.

      Attribution:
    • mrkeen #1
    • troupo #1
    • skydhash #1

Against the grain

  1. 01

    The article’s toy example hides the real trade-offs

    Abstract examples like doSomething and doSomethingElse make the refactoring look cleaner than it is in business workflows. Once there is real domain setup, real data, and real failure modes, the line between reusable setup and behavior that deserves its own assertion gets much messier. That weakens the article’s practical value even if the underlying instinct is sound.

    Do not adopt the pattern from a toy example alone. Try it on one concrete workflow in your codebase and see whether the tests get easier to read and debug after a few edits.

      Attribution:
    • npodbielski #1 #2
    • nhgiang #1
  2. 02

    Deleting the smaller test can be cleaner

    A few people argued Beck is too attached to keeping each behavior in its own named test. If a larger test already executes the earlier step and your framework reports the exact failed assertion, deleting the smaller overlapping test can reduce ceremony without losing debuggability. The pushback is that this can quietly turn a graph of behaviors into one long path and make later refactors erase coverage by accident.

    If you collapse overlapping tests, make assertion messages and step labels extremely explicit. Then review those larger tests regularly for accidental coverage loss when code or setup changes.

      Attribution:
    • akoboldfrying #1
    • dack #1
    • skydhash #1

In plain english

end-to-end
A test that exercises a full workflow across the whole system from start to finish.
fixture
The data, objects, or environment prepared before a test runs.
GIVEN-WHEN-THEN
A test writing style that separates setup, action, and expected result to make scenarios easier to read.
parameterized tests
Tests that run the same logic multiple times with different input values.
ThreadLocal
A programming mechanism for storing data that is local to a specific thread, which can accidentally leak between tests if threads are reused.
UI
User Interface, the visual and interactive parts of a software product that people use directly.

Reference links

Testing tools and frameworks

  • AVA
    Mentioned as a runner whose async-by-default behavior helps expose order-dependent and timing-related test issues.
  • pytest-reverse
    Shared as a simple way to run tests in reverse order and catch hidden dependencies.
  • JUnit project information
    Referenced to note Kent Beck’s role in creating JUnit.
  • JUnit parallel execution documentation
    Used to explain JUnit’s default sequential behavior and what changes when parallel execution is enabled.

Books

  • Tidy First?
    Recommended as a short, pragmatic book for working in messy codebases.