Skip to content

Unit

The question: is this logic correct — and is this design usable from outside?

The second half is the part worth having. A unit test’s first value is not catching regressions; it is being the code’s first caller. If it is painful to write, the design is telling you something while it is still cheap to listen: too many dependencies, hidden state, a method that does three things, a boundary in the wrong place.

That is why the test-first attitude belongs to development practice rather than to testing. A unit test written after the fact still catches regressions; it just arrives too late to influence the thing it is testing.

Not a class, and not a method. A unit is a behaviour with a boundary you would defend in a code review — often one class, frequently a small cluster of classes that only make sense together.

Testing every class in a cluster individually is how a suite becomes change-detector tests: they assert the current structure rather than the current behaviour, so every refactoring breaks them, and after the third time the team concludes that refactoring is expensive. It is not; over-specified tests are.

The practical rule: test through the boundary the caller uses. If a value object exists only to be passed into an aggregate, test the aggregate.

Property Why
No I/O No network, no filesystem, no database, no clock, no randomness — see determinism
Milliseconds The whole suite runs in the time it takes to lose your train of thought, or it stops being run
Runs anywhere Laptop, aeroplane, pipeline. No environment, no setup, no shared state
Fails to a cause The failure names what is wrong, not “expected true, got false”

A component test is the same typology one boundary out: a module, service or bounded context exercised through its own public interface, with its outbound dependencies replaced by doubles.

Still in-process, still milliseconds, still no environment. What it buys is that it exercises the module’s real internal wiring — its controllers, its mapping, its validation, its error handling — which unit tests over the pieces do not.

This is usually the best value in the whole suite, and the level most often missing. It is where hexagonal or ports-and-adapters designs pay off directly: the ports are exactly where the doubles go. See the architecture patterns in dev-hub.

A double replaces a dependency. It must not replace a dependency’s behaviour with something more convenient than reality — that is how a suite reaches 90% coverage and zero confidence.

  • Stub — returns canned answers. The default; use it unless you need one of the others.
  • Fake — a working, simplified implementation (an in-memory repository). Best for anything the code interacts with repeatedly.
  • Mock — asserts that a call happened. Use only when the call itself is the behaviour under test, such as “an event is published”. Otherwise you are asserting on the implementation.
  • Spy — records calls for later inspection. Same caution as a mock.

The failure to watch for: a stub returning a response the real dependency would never produce. When that is a risk, the check belongs at integration or contract level instead — and by the rule against paying twice, only there.

Anything about composition or reality: whether the query is valid SQL, whether the serialisation matches what the consumer expects, whether the transaction boundary is right, whether the two modules that each pass agree with each other.

Those are the next three typologies, and pushing them into unit tests with a sufficiently clever mock produces the specific failure mode the whole strategy is arranged to prevent — a green suite over a broken system.

See unit and component tooling for the current recommendations per stack, and for the assertion, mocking and property-based libraries that go with each.