Skip to main content
Home
HexagonalGuru
Best Practices & Quality

The Testing Pyramid: A Practical Guide for Suites That Don't Fall Apart

HexagonalGuru's avatar

HexagonalGuru

The Testing Pyramid: A Practical Guide for Suites That Don't Fall Apart

"We have tests," says the team. And it's true: 3,000 tests, a 40-minute suite, seven that fail randomly on Tuesdays, and 85% coverage that doesn't stop bugs from reaching production anyway. The problem is almost never the number of tests: it's where they're placed.

The testing pyramid — the idea Mike Cohn popularized twenty years ago — is still the best answer we have to that question. This guide explains it at the level of detail we use at HexagonalGuru when setting up (or rescuing) the testing strategy of a real project.

The Pyramid

The testing pyramid: many fast unit tests at the base, fewer integration tests in the middle and very few E2E at the top, with cost and speed for each layer

Three layers, each with its own job:

  • Unit tests (the base, ~70%). They test one isolated unit: an entity, a value object, a use case with its dependencies faked. No database, no HTTP, no framework. They run in milliseconds.
  • Integration (the middle, ~20%). They test that the pieces fit with real infrastructure: the repository against a real database, the HTTP adapter against the router, the external API client against a sandbox.
  • E2E (the top, ~10%). They test the whole system the way a user would: login, purchase flow, checkout. Real browser, full stack up. Slow, fragile and expensive — which is why they're few and reserved for critical paths.

The shape is not decorative: it's the direct consequence of three factors that change with each layer — speed (milliseconds → seconds → minutes), diagnostic precision (a broken unit test tells you the line; a broken E2E tells you "something fails at checkout") and maintenance cost (every refactor breaks E2E by the dozen).

What Goes in Each Layer (with Real Examples)

Unit: the business logic

Everything that is a rule of your domain lives here. If you followed our series on DDD and hexagonal architecture, this rings a bell: core rules get tested in isolation, with in-memory adapters, booting nothing:

public function test_an_empty_order_cannot_be_confirmed(): void
{
    $order = Order::create(OrderId::new(), CustomerId::new());

    $this->expectException(EmptyOrder::class);
    $order->confirm();
}

This test runs in microseconds and says exactly which rule broke if someone touches it two years from now. Hundreds of these make a solid base.

Integration: the edges

public function test_the_repository_saves_and_retrieves_the_aggregate(): void
{
    $order = Order::create(OrderId::new(), CustomerId::new());
    $order->addLine(ProductId::new(), Money::euros(19.99), 2);

    $this->repository->save($order); // real database (testcontainer)

    $retrieved = $this->repository->find($order->id());
    self::assertEquals('3998', $retrieved->total()->cents);
}

This is where you catch ORM mapping errors, broken migrations and the N+1 queries a unit test can't see.

E2E: only the paths that pay the servers

Login, signup, purchase — the flow that, when it breaks, gets the CEO calling you. One or two per critical feature, with controlled seed data, accepting their cost: they're the fire alarm, not the smoke detector in every room.

The Anti-Pyramids: How This Breaks

Testing anti-patterns: the ice cream cone (all E2E) and the coverage obsession versus the healthy pyramid
  • The ice cream cone (inverted pyramid). Few unit tests, many E2E. The suite takes an hour, fails for reasons unrelated to the code, nobody runs it locally and in the end nobody believes it. The most common one in teams that "started testing with Selenium".
  • Coverage theatre. 90% coverage built with assertion-free tests, proudly tested getters and mutations that would survive every test. Coverage measures which lines were executed, not which behavior was verified.
  • The unit test that mocks everything. You mock the database, the repository and half the domain: the test always passes and proves nothing. If everything is a mock, the test is a poem about how you'd like the code to be.
  • Testing implementation, not behavior. "This private method was called twice": innocent refactor, broken test. Tests must survive refactors; that's why you test the what, not the how.

Common Mistakes

  1. Building the pyramid from the top. Setting up Cypress before having a single domain test. It's building the roof first: impressive, but it holds nothing.
  2. Tests that depend on order or shared data. They pass sequentially, fail in parallel. Each test creates its own data and touches nobody else's.
  3. The slow suite as a tax. If the suite takes 40 minutes, it runs once a day and bugs get caught hours after being written. Suite speed is a feature: budget it and defend it (unit in seconds, integration in minutes, E2E aside).
  4. Tolerated random failures. "Re-run it and it'll pass" is the death of trust in the suite. A flaky test gets fixed or deleted; there's no healthy third option.
  5. Testing only the happy path. Bugs live at the edges: the empty order, the duplicate email, the declined payment. One exception test is worth three success ones.

How to Start (or Start Over)

  1. Identify the 3 critical business flows and write one E2E for each. You now have a fire alarm.
  2. Move the domain logic into the core (if it isn't there) and cover it with fast unit tests. It's the layer with the best return per line written.
  3. Add integration tests at the repositories and adapters touching real infrastructure.
  4. Team rule: every fixed bug comes with its regression test. The suite grows exactly where the system has proven it breaks.
  5. Measure suite time as a product metric, not a technical detail.

The Bottom Line

A good testing strategy is not "more tests": it's every test in its layer. Many fast unit tests for the logic, just enough integration at the edges, a few E2E for critical paths — and a suite the team trusts because it's fast, stable and only fails when something is genuinely broken. The pyramid is not a pretty drawing: it's the shape of testing that doesn't hurt.

Does your suite take forever, fail for sport or simply not exist? In our quality audits we measure the real state (speed, flakiness, useful coverage) and leave you a layer-by-layer plan. Let's talk →

Keep reading: The OWASP Top 10 explained with real examples — starting with the authorization tests that catch IDOR.

  • #testing
  • #quality
Related articles
Shall we start?

Ready to build something that grows with your business?

Tell us your goals and together we will map the design, development and technology route that takes you from vision to measurable results.