🇬🇧 English | đŸ‡Ș🇾 Español
← Back to Blog

Layered Testing Approaches: Knowing What You Already Did

Published on February 25, 2026 by Editorial Team 1 min read
Tags: testingstrategypyramidintegrationunit testingTDDATDDGherkin

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.