← tech

// engineering

Beyond coverage: testing with confidence

A practical guide to unit, component, and integration tests, test doubles, and isolation.

|3 min read|
testingengineering

Senior engineers do not maximize coverage. They maximize confidence per unit of effort. The pyramid reflects that tradeoff: cheap tests at the base, expensive tests at the top.

End-to-end
~2%
Integration
~10%
Component
~15%
Unit
~75%

The three layers

Unit tests cover a single function or class in isolation. Every dependency is replaced with a test double. This is where business logic lives: branches, edge cases, failure modes. If a method needs five mocks to test, that is a design problem.

Component tests cover a meaningful slice together: a handler, a service, and their interactions, with only the outermost dependency mocked (usually a database or external API). This catches bugs unit tests miss, like a handler passing the wrong argument to a service.

Integration tests verify your code works with real infrastructure. Real database. Real message bus. Do not re-test business logic here: only verify the plumbing works and data round-trips correctly.

Mocking

Mocking is essential but dangerous when overused. A mock returns what you told it to return. A real dependency can surprise you.

StubReturns a canned value. Use when you just need a dependency to return something.
MockVerifies a method was called with specific arguments. Use when the call itself is the behavior under test.
FakeA lightweight real implementation: an in-memory database, a fake queue. More realistic than a stub.

Mock external services and infrastructure. Do not mock your own internal classes. Prefer fakes for complex dependencies.

Smells to watch for: mocking five things to test one method (design is too coupled), or high coverage with frequent production bugs (your mocks do not reflect reality).

Isolation and the AAA pattern

Each test must set up its own state, run independently, and clean up after itself.

ArrangeSet up everything the test needs
ActCall the code under test
AssertVerify the outcome

Shared state creates flaky tests: passing locally, failing randomly in CI. Flaky tests erode trust. For integration tests with a real database, reset state between tests or wrap each in a transaction that rolls back.

The bottom line

If your code is hard to test without heavy mocking, that is a design signal. Refactor toward dependency injection and single responsibility. Testability improves as a side effect. And no test suite catches everything. Pair it with observability and production monitoring.