What the test pyramid is
The test pyramid is a guideline for balancing automated tests. Write many fast unit tests, fewer integration tests and a small number of end-to-end tests. Tests near the base are cheap to run and pinpoint failures, while tests near the top cost more but exercise more of the real system.
Mike Cohn introduced the idea in his 2009 book Succeeding with Agile. The pyramid gives no exact ratios. Its advice is to test each behavior at the cheapest level that can catch the bug.
Unit, integration and end-to-end tests
Names vary between teams, so agree on definitions within your codebase. What separates the three levels is how much of the system each test runs:
- Unit tests check one function or class in isolation, with slow or external parts replaced by fakes. They run in milliseconds and point straight at the broken code, but they can't show that components work together.
- Integration tests check a component against the real things it talks to, such as a repository against a database running in a container. They catch wrong queries, missing migrations and mismatched contracts between modules.
- End-to-end tests drive the whole application the way a user does, often through a browser with a tool such as Playwright or Cypress. They check that the parts are wired together, but they are slow and fail for many reasons unrelated to the code under test.
Why the pyramid has that shape
Speed drives most of it. A suite of thousands of unit tests can finish in seconds, so developers run it on every change. End-to-end suites take minutes or longer, so they run less often and report problems later.
Failures at the base are also easier to act on. A failed unit test names the function and the assertion. A failed end-to-end test says checkout broke somewhere in the browser, the network, the API or the database.
End-to-end tests still earn their place for the few journeys that must never break, such as sign-in and payment. Cover those flows end to end, and test the edge cases of each step lower down.
The testing trophy, from Kent C. Dodds, is a common counterpoint that puts most effort into integration tests, arguing they give the best balance of confidence and cost.
Flaky tests and how to fix them
A flaky test passes and fails on the same code. Its cost goes beyond reruns: once people learn to rerun red builds, they start ignoring real failures too.
Fix the cause rather than adding automatic retries, which can hide a real race condition in your code. If you can't fix a flaky test right away, quarantine it with a named owner and a deadline, so it neither blocks everyone nor quietly disappears.
Most flaky tests come from a few causes:
- Fixed sleeps instead of waiting for a condition, such as an element appearing or a job finishing
- Shared state between tests, such as database rows or global variables, so results depend on test order
- Dependence on the current time, time zones, random values or real network calls
- Race conditions in the code itself, which the test exposes only some of the time
What to automate in CI first
Automate the cheapest checks that catch the most problems first, then add slower layers as the suite matures. If you are starting from nothing, this order works for most web projects:
- Build, type checking and linting on every push. They take little time and catch whole classes of mistakes.
- Unit tests on every push and pull request, kept fast enough that nobody is tempted to skip them.
- Integration tests against real dependencies, such as a database started in a container for the test run.
- Required status checks on the main branch, so a failing build blocks merging.
- A few end-to-end smoke tests for critical journeys, run on each pull request or before each deploy.
- The full end-to-end suite nightly or before a release, if it is too slow for every change.
- A regression test with every bug fix, written at the lowest level that reproduces the bug.