f in x
Testing Pyramid: The Right Ratio of Unit, Integration, and E2E Tests
> cd .. / HUB_EDITORIALE
Analisi dei dati e metriche

Testing Pyramid: The Right Ratio of Unit, Integration, and E2E Tests

[2026-05-31] Author: Ing. Calogero Bono

The problem: tests that waste your time or miss the bugs

Your test suite runs for an hour. Or it always passes, but bugs slip into production. Both mean your proportion is wrong. At Meteora Web, we see this often: teams write only end-to-end tests because "they simulate the user", then get 40-minute builds. Others cover only isolated functions with unit tests, and discover in production that modules don't talk. The testing pyramid isn't a dogma — it's a thermometer for speed vs. reliability. Ignore it, and you pay in wasted time or undetected bugs.

Why the pyramid exists

Imagine a test that runs the entire purchase flow: login, catalog, cart, checkout. If it fails, you don't know if it's the payment gateway or a frontend error. Debugging takes 20 minutes. The pyramid says: small, fast tests should be numerous; large, slow tests should be few. The classic ratio is 70/20/10: unit 70%, integration 20%, E2E 10%. But reality is nuanced, and depends on your domain.

What happens if you invert the pyramid

A client had an E2E suite for every feature, zero unit tests. A CSS change broke a test about a distant button. Result: 3 hours of fixes per week and zero confidence. We flipped the pyramid: unit tests for business logic, integration tests with mocked external services, E2E only for critical flows. Tests passed in 5 minutes. Team morale rose, bugs dropped.

Unit tests: the foundation

A unit test verifies a single unit of code — a function, a method — in isolation. It must be fast (milliseconds), deterministic, and without external dependencies (no database, API, filesystem).

Example in JavaScript with Jest:

// function to test
function calculateDiscount(price, percent) {
  if (percent < 0 || percent > 100) {
    throw new Error('Invalid percent');
  }
  return price - (price * percent / 100);
}

// test
test('20% discount on 100 gives 80', () => {
  expect(calculateDiscount(100, 20)).toBe(80);
});

This test runs in milliseconds, needs nothing. If broken, you know exactly which function is guilty.

The limit of unit testing

Alone they aren't enough. The calculateDiscount function might work, but if the frontend sends the percent as a string or backend never calls it, the bug escapes. That's why you need integration tests.

Integration tests: the glue

Integration tests verify that two or more modules interact correctly. They often include a test database, an in-memory HTTP server, or mocked external services. Slower than unit tests (seconds), but much faster than E2E.

Example with Node.js, Express, and supertest:

const request = require('supertest');
const app = require('../app');

describe('POST /api/orders', () => {
  it('should create an order with valid data', async () => {
    const res = await request(app)
      .post('/api/orders')
      .send({ customer: 'Mario', amount: 50 });
    expect(res.statusCode).toBe(201);
    expect(res.body.id).toBeDefined();
  });
});

Here we're testing that the server responds correctly to an HTTP request, validation works, data reaches the DB. No browser needed. If the payment gateway is mocked, the test stays fast and reliable.

Caution: mock and stub

Don't mock everything. A common mistake is isolating tests so much they no longer test real integration. At Meteora Web, we mock only services we don't control (third-party APIs, email), and use real databases in Docker containers for the rest.

E2E tests: the sanity dashboard

End-to-end tests simulate a real user: open a browser, click, fill forms, wait for responses. They are slow (minutes), brittle (a CSS class change breaks them), and expensive to maintain. But they give maximum confidence: if they pass, the flow works for real.

Example with Cypress (minimal setup):

describe('Purchase flow', () => {
  it('completes an order', () => {
    cy.visit('/');
    cy.contains('Buy').click();
    cy.get('input[name="email"]').type('test@example.com');
    cy.get('button[type="submit"]').click();
    cy.url().should('include', '/thank-you');
  });
});

This test runs through the whole app: frontend, API, database, external services. It's expensive: run only on critical flows (checkout, login, registration).

The right proportion: how to calculate

There's no universal magic number, but a rule of thumb that works: every failing test should point to the error in under 5 minutes. If an E2E test fails and you must dig through 10 files, too many. If a unit test fails and you see the bug immediately, it's gold.

Start with these guidelines:

  • Unit tests: cover all business logic. If a function has conditionals, test every branch. Aim for >80% coverage on business rules (not cosmetic code).
  • Integration tests: cover every API endpoint and every database interaction. At least one positive and one error-expected test per flow.
  • E2E tests: one test per critical journey (login, purchase, registration). Maximum 10-15 tests total.

Adapting the pyramid to your project

If you work on an app with very little logic (e.g., simple CRUD), the proportion shifts toward integration and E2E. If you have a rich domain (finance, insurance), unit tests dominate. At Meteora Web, we built proprietary platforms with Laravel: for billing modules (VAT rules, tax calculation) we write dozens of unit tests, while for the Vue frontend we stop at a few integration tests and one E2E for onboarding.

Common mistakes to avoid

  • Testing implementation: testing that a function calls another internal function instead of testing behavior. Implementation tests break on every refactor and add no value.
  • Over-mocking: if an integration test mocks the database too, it's no longer an integration test — it's a disguised unit test.
  • Omnipresent E2E: testing every button with Cypress leads to a slow, brittle suite. Be selective.
  • Ignoring setup: a test requiring 30 seconds of fixtures won't be run willingly. Automate test data with factories and seeds.

Practical tools to start

For JavaScript/TypeScript: Jest or Vitest for unit, supertest for integration, Cypress or Playwright for E2E. For PHP/Laravel: PHPUnit for unit and integration, Dusk for E2E. For Python: pytest with pytest-django and Selenium or Playwright.

An operational tip: don't write tests for features that don't exist yet. Wait until the interface is stable, then test it. And don't chase 100% coverage in every file — focus on areas that generate revenue: pricing logic, shipping calculations, payment integrations. As we always say: "A site is measured in revenue, not compliments." Tests protect that revenue.

In summary — what to do now

  1. Analyze your current suite: count how many tests of each type you have. If the unit/integration/E2E ratio is more than 1:1:1, you have a problem.
  2. Shift tests down: convert brittle E2E tests into robust integration tests. Simple flows don't need a browser.
  3. Add unit tests for business rules: identify functions with conditionals, calculations, validations. Write a test for every path.
  4. Automate execution: tests must run on every push (CI/CD). If you have no pipeline, start with GitHub Actions and a minimal workflow.
  5. Measure execution time: a suite that takes more than 10 minutes will be run less and less. Cut superfluous E2E tests.

The testing pyramid is a living tool. It doesn't apply the same way to an e-commerce site and an ERP. But the principle is universal: test small things a lot and fast; test big things a little and slow. We apply it every day on our clients' projects. If you want to see it in action on a real project, check out our guide on API design — we integrated this approach there.

Sponsored Protocol

Ing. Calogero Bono

> AUTHOR_EXTRACTED

Ing. Calogero Bono

Co-founder di Meteora Web. Ingegnere informatico, sviluppo ecosistemi digitali ad alte prestazioni. AI, automazione, SEO tecnica e infrastrutture web. Scrivo di tecnologia per rendere complesso… semplice.

[ Read Full Dossier ]

Hai bisogno di applicare questa strategia?

Esegui il protocollo di contatto per iniziare un progetto con noi.

> INIZIA_PROGETTO

Sponsored

> MW_JOURNAL

> READ_ALL()