Layered Testing Approaches: Knowing What You Already Did
Testing strategies are often visualized as a pyramid to represent the distribution of different types of tests. While the âStandard Pyramidâ is a well-established standard for testing, different architectural priorities can be addressed with âTop-Down,â âBottom-Up,â and âHourglassâ models.
Each approach listed here is essentially a different philosophy on where to invest your most time, which is usually the most expensive resource.
Top-Down Testing
In this approach, testing starts with the highest-level modules (the UI or the main control flow) and works its way down to the individual units.
How it works: You use âStubsâ to simulate the lower-level modules that arenât built or integrated yet.
Pros:
- High Visibility: Stakeholders see a âworkingâ (albeit hollow) product early.
- Interface Validation: You catch major architectural flaws in how the systemâs main components interact early on.
Cons:
- Stub Overhead: Writing and maintaining complex stubs for every low-level function can be massive time sink.
- Late Logic Catch: Critical bugs in the deep âengineâ of the app might not be found until the very end.
Bottom-Up Testing
This is the inverse: you start by testing the smallest, lowest-level units (drivers, utility functions, database helpers) and gradually integrate them into larger subsystems.
How it works: You use âDriversâ to simulate the higher-level calls that would normally trigger these units.
Pros:
- Early Robustness: By the time you reach the UI, the foundation is rock-solid.
- Easy Debugging: If a test fails at this level, you know exactly which function is broken.
Cons:
- Invisible Progress: You might have 100% passing tests but still no âappâ to show the client.
- Interface Risk: You might find that your perfectly tested units donât actually fit together at the top level (the âTwo-Unit-Tests-Zero-Integration-Testsâ problem).
The Hourglass
The Hourglass occurs when a team has many Unit tests and many End-to-End (E2E) UI tests, but almost no integration tests that show the individual units cooperate as aggregate subsystems.
How it works: The âwaistâ of the pyramid disappears. You trust the units and you trust the final UI flow, but you donât test the API or service layer where they meet.
Pros:
- Fast Unit Tests: Developers get quick feedback on local code.
- User-Centric: E2E tests ensure the âHappy Pathâ works for the customer.
Cons:
- Slow and Brittle: Poorly implemented UI tests are notorious for being slow to run and potentially non-deterministic. If the middle layer is missing, the E2E tests donât always give much actionable information as to the nature of any problems.
- The âGapâ Problem: If an API change breaks a contract but doesnât immediately crash the UI, you might not catch the bug until it hits production. This may be interpreted as a missing acceptance test case for that interaction, but this happens often enough that some authors regard Hourglass testing as an antipattern.
Comparison Summary
| Style | Primary Focus | Main Tool | Risk Level |
|---|---|---|---|
| Top-Down | User Experience | Stubs | High (Core logic bugs found late) |
| Bottom-Up | Code Quality | Drivers | Medium (Integration bugs found late) |
| Hourglass | Extremes (UI & Unit) | Scripts | High (Missing the "Integration" bridge) |
Which should you choose?
My usual preference is for acceptance-test driven development, using Gherkin language behavioural specifications to start testing in a top-down manner. If the acceptance criteria cover all the required behaviors and the step implementations work well enough the ATDD approach should give a system that does everything the customer needs. By migrating groups of acceptance tests into regression packs after each release version, we can keep validation of current implementation fast enough in development while retaining coverage over all use-cases that can be executed only on creation of formal release candidates.
I like to code acceptance tests so they can target any deployment. This allows parts of their logic, potentially just the code of their steps, to be reused as parts of operational diagnostics, or system driving logic to support debugging of live systems.