Blog / Technical / The Testing Pyramid

The Testing Pyramid

The testing pyramid describes the optimal distribution of test types by count and purpose. Many fast, cheap unit tests form the base; fewer, slower integration tests form the middle; a small number of end-to-end...

large-scale-software-engineeringdocs

The testing pyramid describes the optimal distribution of test types by count and purpose. Many fast, cheap unit tests form the base; fewer, slower integration tests form the middle; a small number of end-to-end tests form the top. This distribution maximizes feedback speed while ensuring broad coverage.

Testing Pyramid

graph TD subgraph Pyramid[Testing Pyramid] E2E[End-to-End Tests\nFew - 5 to 20\nSlowest - minutes\nMost expensive\nTest complete user journeys\nin real environments] Integration[Integration Tests\nModerate - 50 to 200\nModerate speed - seconds\nTest module interactions\nExternal dependencies\nor test doubles] Unit[Unit Tests\nMany - 500 to 5000\nFastest - milliseconds\nCheapest\nTest individual functions\nin total isolation] end E2E --> Integration --> Unit style E2E fill:#fee2e2,stroke:#dc2626 style Integration fill:#fef3c7,stroke:#d97706 style Unit fill:#dcfce7,stroke:#16a34a,stroke-width:2px

Unit Testing

graph LR subgraph UnitTest[Unit Test Anatomy - AAA Pattern] Arrange[Arrange\nSetup test data\nCreate test doubles\nConfigure dependencies] Act[Act\nCall the function or method\nbeing tested] Assert[Assert\nVerify the result\nVerify side effects\nVerify exceptions] Arrange --> Act --> Assert end subgraph UnitTestProperties[Good Unit Test Properties] Fast[Fast: runs in microseconds] Isolated[Isolated: no network, no disk, no DB] Deterministic[Deterministic: same result every run] Focused[Focused: tests one behaviour] end

Integration Testing Approaches

graph TD subgraph IntegrationTypes[Integration Test Types] ServiceInt[Service Integration\nTest service + real DB\nDocker Compose for dependencies\nTestContainers for ephemeral DBs] ContractInt[Contract Testing\nTest service against\nconsumer-defined contracts\nPact framework] APIInt[API Integration\nHTTP-level tests against\nrunning service\nTest request-response contracts] end subgraph TestContainers[TestContainers Pattern] TC[TestContainers library\nSpins up real Docker containers\nfor test dependencies\nPostgres Kafka Redis etc\nTeardown after tests] TC --> ServiceInt style TC fill:#dcfce7,stroke:#16a34a end

End-to-End Testing

sequenceDiagram participant E2E as E2E Test participant Browser as Browser / Playwright participant Frontend as Frontend participant API as API Service participant DB as Database E2E->>Browser: Navigate to /checkout Browser->>Frontend: GET /checkout Frontend-->>Browser: Checkout page HTML E2E->>Browser: Fill in card details Browser->>Frontend: Form data Frontend->>API: POST /api/orders API->>DB: INSERT order DB-->>API: order_id: 123 API-->>Frontend: 201 Created Frontend-->>Browser: Success page E2E->>Browser: Assert: success message visible E2E->>API: GET /api/orders/123 E2E->>E2E: Assert: order status = confirmed

Key Concepts

  • Testing Pyramid: The pyramid shape reflects the ideal distribution: many fast unit tests (broad coverage, fast feedback), some integration tests (verify components work together), few E2E tests (verify complete user journeys). An "ice cream cone" (many E2E, few unit tests) is the anti-pattern — slow, flaky, expensive to maintain.

  • Unit Tests: Test a single function, method, or class in complete isolation. All external dependencies (databases, HTTP calls, file systems) are replaced with test doubles. The goal: every code path and edge case has a corresponding fast test. Coverage target: 80%+ statement coverage.

  • Integration Tests: Test multiple components working together. Can test service + real database (using TestContainers), service + message broker, or API endpoint behaviour. Slower than unit tests but catch integration bugs (SQL schema mismatches, serialization errors, configuration mistakes) that unit tests cannot.

  • End-to-End (E2E) Tests: Test complete user workflows through the deployed application. Playwright, Cypress, or Selenium drive a real browser. Selenium Grid or Playwright in CI runs against a staging environment. E2E tests catch real user-visible regressions but are slow (minutes), fragile (UI changes break tests), and expensive to maintain.

  • Test Coverage: A measure of how much production code is executed by tests. Statement coverage (% of lines executed) and branch coverage (% of conditional branches covered) are the standard metrics. High coverage does not guarantee correct tests — tests can execute code without asserting correctness.

  • Flaky Tests: Tests that produce different results on successive runs with the same code. Usually caused by time dependencies, async timing issues, shared state between tests, or non-deterministic data. Flaky tests erode confidence in the test suite. Track and fix flakiness aggressively.

  • TestContainers: A library (available for Java, Python, Go) that manages Docker containers for test dependencies. Instead of mocking a database, spin up a real Postgres container for the test session and tear it down after. Eliminates mock-vs-reality discrepancies.

Trade-offs

Test Type Speed Cost Confidence Maintenance
Unit Very fast Low Code logic Low
Integration Moderate Medium Component interaction Medium
E2E Slow High User journey High

When to Apply

  • Write unit tests first for all business logic, domain models, and utility functions
  • Add integration tests for service boundaries — how your service talks to databases, message brokers, and other services
  • Limit E2E tests to critical user journeys (registration, checkout, core workflow) — the scenarios where failure is most painful
  • Target 70-80% unit test coverage; 90%+ of E2E tests must be green before every production deployment