concept

Testability

also called Design for Test

The architectural property that determines what kinds of test are possible - which decides the shape of the suite far more than any testing discipline does.

testingarchitecturedeterminismboundariesdependency-injection

Testability is not a property of a test suite. It is a property of the code: whether a component can be constructed in isolation, whether its dependencies can be substituted, and whether its behaviour is deterministic.

A system with poor testability forces broad tests, because narrow ones are impossible to write. Broad tests are slow, flaky and owned by nobody — so the suite grows without the confidence growing, and adding more tests produces more of what is already not working.

The properties that constitute it

1. Constructibility. Can a component be created without also creating the world? A class that reaches for a global connection, reads ambient configuration at construction, or instantiates its own collaborators cannot be tested alone.

2. Substitutable dependencies. Collaborators supplied rather than acquired, so a test can provide a stub. This is the practical form of dependency inversion and its main payoff.

3. Determinism. No dependence on wall-clock time, randomness, ambient environment or network state — or, where those are needed, they are injected so a test can control them. This single property removes the largest category of flakiness, and flakiness is what destroys the value of an entire suite.

4. Observable outcomes. A component whose effect is only visible in an external system requires an integration test; one that returns a result or exposes state can be tested directly.

5. Enforced invariants at the data layer. An invariant expressed as a conditional update or a constraint needs far less testing than one enforced by reading, deciding and writing — because the second has a race that tests find only sometimes.

Industry example

The characteristic symptom is an inverted test pyramid: many end-to-end tests, few unit tests. Teams are frequently blamed for the shape, and the shape is usually rational — those are the tests that catch defects in a system whose units cannot be isolated.

The productive intervention is therefore architectural rather than procedural: inject the dependencies, give components their own data, remove ambient configuration. Only then does rebalancing the suite become possible, and the contract-test layer that replaces the expensive middle becomes viable.

The same reasoning explains why a local development environment that requires many services to run is an architecture signal rather than a tooling problem — if a change cannot be developed and tested against one service with stubs, the boundaries are wrong.

Failure scenarios

  • Adding tests to an untestable architecture, producing a slow flaky suite.
  • Mandating coverage targets, which drives tests that execute code and assert nothing where coverage is cheap.
  • Blaming flakiness on test authors when the cause is non-injected time, randomness or environment.
  • Building elaborate test infrastructure to work around untestable code, which entrenches it.

Trade-offs

Designing for testability adds indirection — injected dependencies, interfaces, explicit clocks — which is genuine cost and can be overdone into speculative abstraction. The discipline is to inject what varies for testing purposes, not to abstract everything.

For a small script or a genuinely disposable component, the cost is not justified. For anything with a meaningful lifetime and multiple changers, it determines whether a fast reliable suite is achievable at all.

Interview question

"Your suite is slow and flaky, and removing tests causes escapes. Walk me through what you would change — and tell me why writing more unit tests would not fix it."