After separating UI-shaped reads from domain-shaped writes, the next question is how to test those boundaries. A useful test suite is part of the development loop, protects the code that matters, and returns enough confidence to justify its maintenance cost. None of those requirements says “coverage must reach a number that looks impressive in a quarterly slide.”
Coverage can expose untested code. It cannot tell you whether the tests describe valuable behavior, catch plausible regressions, or survive a refactor. A thousand assertions around trivial getters are still a thousand things to maintain.
Three familiar shapes #
The ice-cream cone puts most effort into manual and end-to-end tests, with fewer integration and unit tests. It favors realism, but today’s E2E tests remain slower, more fragile, and more expensive to diagnose. Used as the default strategy, the cone tends to turn the release process into a dessert-themed queue.
Diagram source: gihyo.jp’s Savanna Letter series.
The testing pyramid builds a broad base of unit tests, a smaller integration layer, and a thin E2E layer. Fast, stable checks cover most behavior; expensive tests are reserved for the journeys only a real stack can prove.
Diagram source: gihyo.jp’s Savanna Letter series.
The testing honeycomb was proposed for microservices. It emphasizes integration at the service boundary, de-emphasizes internal implementation details, and avoids broad tests that depend on several live services. A service should be verified deeply within its own boundary rather than requiring the whole company to be awake.
Diagram source: Spotify Engineering.
These shapes are heuristics. A monolith with substantial domain logic may favor the pyramid. A thin service whose primary job is integration may reasonably spend more of its budget at boundaries.
Agree on what “unit” means #
Consider a Rails model spec that reads and writes the test database. The classical school may still call it a unit test if it verifies one observable behavior. The London school calls it an integration test because a real dependency is involved. Google’s test-size vocabulary calls it Medium because it uses a database.
All three descriptions can be internally consistent. The dangerous state is a team saying “we need more unit tests” while each person silently means a different boundary.
Google’s size model is operationally crisp:
| Capability | Small | Medium | Large |
|---|---|---|---|
| Network | None | Localhost | Allowed |
| Database/filesystem | None | Local machine | Allowed |
| External systems | None | Prefer none | Allowed |
| Threads and sleep | None | Allowed | Allowed |
| Typical upper time | 60 s | 300 s | 900+ s |
Mock at architectural boundaries #
A test double replaces a dependency used only during testing. A stub supplies input to the system under test. A mock verifies output toward another system.
Mocks make tests faster and isolate failures, but extensive mocking couples the suite to implementation details. A harmless refactor then breaks tests that were really testing the wiring diagram. Prefer real domain objects for in-process collaboration. Mock calls that leave your ownership boundary: payment providers, email services, or another company’s API.
The most valuable tests usually protect business rules. Infrastructure adapters, framework glue, and generated plumbing may need fewer direct tests unless failure there carries unusual risk.
Write the local constitution #
A test strategy should answer, in the team’s language:
- Which behaviors are expensive or dangerous to get wrong?
- What counts as unit, integration, and end-to-end here?
- Which dependencies may each test class use?
- Where are mocks acceptable?
- How fast must each suite be to remain in the feedback loop?
Choose the shape after answering those questions. The pyramid is a useful sketch, not a building code.