AV
HomeAboutProjectBlog

© 2026 Ave syah Shina. All rights reserved.

  1. Home
  2. Blog
  3. 17 — Testing — The Pyramid, and What Each Layer Catches

17 — Testing — The Pyramid, and What Each Layer Catches

August 13, 20268 min read
Download as Markdown

"Write tests, any tests" was my testing policy until the first suite that passed while the app was still broken. The structure that separated them: the levels aren't interchangeable, they're a pyramid where each layer catches a different class of bug, and the shape — many unit, fewer integration, fewest end-to-end — optimizes for fast feedback at the bottom and high confidence at the top. [1]

The frame that helped is what can fail here, and how fast do I find out. A unit test isolates one function and runs in milliseconds; an integration test checks that two modules talk correctly and runs in tens of milliseconds; an end-to-end test drives the whole system through the browser and runs in seconds. Each layer's cost (writing time, run time, flakiness) rises as it broadens, and each layer's confidence rises with it. The pyramid isn't a rule about counting tests; it's a rule about where to spend each kind of effort.

Functional / E2E fewest tests · seconds each catches whole-flow bugs Integration fewer tests · tens of ms catches wiring / contract bugs Unit many · milliseconds whole-flow rendering, real HTTP module boundaries, API contracts, DB schema pure logic, edge cases, single-function behavior confidence ▲ cost / flakiness ▲

Unit testing: the wide base

Unit testing tests individual components — the smallest testable units, usually functions — in isolation [3]. I give the function inputs and assert the outputs; dependencies (other functions, the database, the network) are replaced with test doubles (mocks, stubs) so the test exercises only the function's logic.

The properties of a good unit test:

  • Fast — milliseconds. A unit test suite of hundreds of tests should run in under a second.
  • Isolated — no dependency on the database, the filesystem, or the network. If it needs those, it's an integration test.
  • Specific — one assertion focus per test, named for the behavior it verifies ("returns 0 for empty input," not "test1").

Unit tests catch the class of bugs that live inside a single function: wrong calculation, off-by-one, unhandled branch, edge case (empty, null, max). They don't catch bugs at module boundaries — those are integration tests' job. The strength of unit tests is that they're cheap enough to run on every save, so feedback is immediate, and there can be many of them covering edge cases exhaustively. The weakness is that a function passing its unit tests in isolation tells me nothing about whether it's called correctly by the rest of the system.

Integration testing: the middle band

Integration testing verifies that components work together — module communication via APIs, database reads and writes, third-party service calls [2]. Where the unit test stubs the database, the integration test uses a real (often test-instance) database. Where the unit test mocks the payment provider, the integration test checks that the code constructs the right request shape.

The bugs integration tests catch are exactly the ones unit tests miss:

  • Contract mismatches — module A calls module B with the wrong argument shape.
  • Schema drift — the code expects a column the database doesn't have (the migration wasn't run).
  • Wiring bugs — the route is wired to the wrong controller, the dependency injection is misconfigured.
  • Real I/O issues — the query returns rows in an order the code didn't expect.

Integration tests are slower than unit tests (real I/O, real setup) and more brittle (they depend on more state), so there are fewer of them. The common tool for HTTP integration testing in Node is Supertest — it boots the app and makes real HTTP requests against it, asserting on the response, without a browser.

Functional / end-to-end: the narrow top

Functional testing validates the system from the outside — typically by driving the application through its public interface (an HTTP endpoint, a browser) and asserting on the observable behavior [4]. End-to-end (E2E) tests go further, driving a real browser through the full stack — frontend, API, database — to simulate a user flow.

The bugs functional/E2E tests catch:

  • Whole-flow failures — the login form submits, the session is set, the redirect happens, the dashboard renders. Any break anywhere in the chain fails the test.
  • Render-time issues — the right data is in the DOM, the button is clickable, the error message appears.
  • Real user paths — the things users actually do, end to end, that no lower-layer test reproduces.

The cost: E2E tests are slow (seconds each), flaky (browser timing, async races), and expensive to maintain. A suite of thousands of E2E tests is a well-known anti-pattern — slow CI, constant flake-triage. The pyramid says: few E2E tests, covering only the critical user paths; rely on integration and unit tests for breadth. The dominant tool for E2E in the modern stack is Playwright, which automates real browsers across Chromium, Firefox, and WebKit.

The pyramid as a spending guide

The pyramid isn't dogma ("exactly 70% unit, 20% integration, 10% E2E"); it's a heuristic about where each kind of test pays off. The principle:

  • Many unit tests — cheap, fast, exhaustive on edge cases. The bulk of the suite.
  • Fewer integration tests — one per important module boundary, covering the contract. The middle.
  • Fewest E2E tests — one per critical user flow (login, checkout, core feature). The tip.

The reason for the shape is economics. A bug found by a unit test is found in milliseconds, on the line that caused it. The same bug found by an E2E test is found in seconds, somewhere in a multi-step flow, and I have to debug which layer caused it. Cheaper tests catch more bugs per unit of effort, so I spend the bulk there; expensive tests are reserved for what only they can verify.

A common modern counter-pattern is the "testing trophy" — more integration tests, fewer unit tests — on the grounds that integration tests catch the bugs that actually ship (the wiring ones) and aren't much slower than unit tests for typical web apps. The trophy is a reasonable adjustment, but the underlying principle is the same: spend test effort where each kind of bug is cheapest to catch, and don't rely on a single layer.

How I use this

Three rules capture the practice:

  • Write the unit test first when the logic is pure. Functions with clear inputs and outputs get unit tests; edge cases get exhaustively covered. This is the bulk of the suite.
  • Write an integration test for every API endpoint and every module boundary. The contract between layers is where the worst bugs hide; one integration test per boundary catches them.
  • Reserve E2E for the critical user paths. Login, checkout, the one feature the product depends on. A handful, not a mountain.

The discipline I apply across all three: every bug I ship becomes a test that would have caught it, at the lowest layer that could have caught it. The pyramid isn't about test counts; it's about building a net where each hole size is appropriate to its layer — fine at the bottom (unit, many small checks), coarse at the top (E2E, few whole-flow checks). The combination is what lets me ship with confidence that the obvious bugs are caught and the critical paths actually work.

References

[1] "Testing Pyramid," BrowserStack. [Online]. Available: https://www.browserstack.com/guide/testing-pyramid-for-test-automation

[2] "Integration Testing," guru99. [Online]. Available: https://www.guru99.com/integration-testing.html

[3] "What is Unit Testing?," guru99. [Online]. Available: https://www.guru99.com/unit-testing-guide.html

[4] "Functional Testing: What It Is and How to Do It Right," Atlassian. [Online]. Available: https://www.atlassian.com/continuous-delivery/software-testing/functional-testing

Knowledge check · Question 1 of 5

Why is the testing pyramid shaped with many unit tests at the bottom and few E2E tests at the top?

Comments

Leave a Comment

You must be signed in to comment

0 Comments

No comments yet. Be the first to comment!