{ } Programming

The testing pyramid: unit, integration and end-to-end tests

What the testing pyramid is and how to balance unit, integration and end-to-end tests to get a test suite that is fast, reliable and genuinely useful.

Once you understand the value of automated tests, the next question is inevitable: which tests should you write, and in what proportion? An application can be checked at very different levels, from a single function up to a simulated user clicking through the browser. Each level has different costs and benefits. The testing pyramid is the best-known model for reasoning about this balance.

The model

The pyramid image, popularised by Mike Cohn, divides tests into three levels:

  1. At the base, many unit tests: they check individual functions or classes, and are fast and cheap.
  2. In the middle, a moderate number of integration tests: they check that several components work together, for example the code and the database.
  3. At the top, few end-to-end tests: they simulate a real user moving through the whole application.

The shape expresses a simple idea: the higher you go, the slower, costlier to maintain and harder to diagnose tests become, so it pays to have fewer of them.

Comparing the levels

Feature Unit Integration End-to-end
Speed Milliseconds Seconds or less Seconds or minutes
What they check Isolated logic Collaboration between parts The user's full flow
Diagnostic precision High Medium Low
Stability High Medium More brittle
Confidence in the real system Limited Good High

The table shows why no level is best in absolute terms. Unit tests tell you exactly where an error is, but do not guarantee that the pieces fit together. End-to-end tests give a lot of confidence, but when they fail it can be hard to understand why.

Integration tests

The middle level is often the most neglected, yet it catches a very common category of error: everything works on its own, but not together. Typical examples:

  • an SQL query that is syntactically correct but does not return the expected data with the real schema;
  • a service expecting dates in one format and receiving another;
  • a configuration missing in the test environment but present in development.

For these tests it is worth using real components in a controlled version: a real database started in a container, with data prepared for each test and cleaned up at the end. Simulating the database with a mock, in these cases, would risk hiding exactly the errors you want to find.

End-to-end tests

An end-to-end test for an online shop might do this: open the home page, search for a product, add it to the cart, complete payment with test data and check the order confirmation. Tools such as Playwright, Cypress or Selenium drive a real browser to carry out these steps.

They are valuable for critical paths, the ones that halt the business if they break: sign-up, login, purchase. But they should be chosen sparingly, for three reasons:

  1. they are slow, and a suite that takes an hour gets run less often;
  2. they are sensitive to details such as load times and animations, and tend to fail intermittently;
  3. when they fail, the cause can be anywhere in the stack.

The ice cream cone anti-pattern

The opposite of the pyramid is the so-called "ice cream cone": very few unit tests, a few integration tests and a large number of end-to-end tests or, worse, manual checks. It is common in projects where tests were added after the fact, starting from what could be seen from outside. The result is a slow, unstable suite that the team learns to ignore.

Getting out of it takes patience: every time an end-to-end test finds a bug, it is worth asking whether the same error could have been caught by a lower-level test, and writing it.

Other models

The pyramid is not dogma. Some developers propose different shapes, such as the "testing trophy", which gives more weight to integration tests, especially for frontend applications where much of the logic lies in the interaction between components. The point common to all models is the same: choose deliberately where to invest, instead of piling up tests at random.

How to apply it in practice

Some concrete guidance for a real project:

  • write unit tests for business logic: calculations, rules, validations, data transformations;
  • write integration tests for the system's boundaries: databases, message queues, external APIs simulated realistically;
  • write end-to-end tests only for flows that absolutely must not break;
  • run unit tests on every change, integration tests on every push, end-to-end tests at least before every release;
  • treat flaky tests as bugs: fix them or remove them, do not ignore them.

In short

  • The pyramid suggests many unit tests, a moderate number of integration tests and few end-to-end tests.
  • The higher you go, the slower, costlier and harder to diagnose tests are, but the more confidence they give in the real system.
  • Integration tests catch errors between components and preferably use real, controlled resources.
  • Reserve end-to-end tests for the application's critical paths.
  • Avoid the "ice cream cone": push checks down a level whenever you can.
MM
Marco Marzaduri

Marco Marzaduri works in cybersecurity, digital forensics and web development. With degrees in computer science and psychology, he combines technical analysis with an understanding of the human factor.

Read also