The test pyramid defines the proportion and purpose of each test layer.
Unit tests are the base of the pyramid: many tests,
fast execution, testing one unit of behaviour in isolation. “Isolation”
means dependencies are replaced with test doubles (stubs, fakes, mocks).
A unit test for calculateDiscount provides an
Order object and a DiscountCode and asserts on
the returned Money — no database, no network. Unit tests
are fast (milliseconds each), reliable (no external state), and targeted
(a failure points to a specific function). They cover pure logic and
business rules.
Integration tests test the behaviour of two or more
components together, or the behaviour of one component against real
infrastructure (a database, a message queue). An integration test for
PostgresOrderRepository.save creates a test database, calls
save, queries the database directly, and asserts on the
stored values. Slower than unit tests (100ms to 1s each), but they catch
the bugs that unit tests cannot: ORM mapping errors, SQL constraint
violations, transaction isolation issues.
Contract tests verify that a service’s published
interface matches what consumers expect. Consumer-driven contract tests
are defined by the consumer: “I expect the orders service to respond
with {id, status, items, total} when I call
GET /orders/:id.” The orders service runs these tests as
part of its test suite, verifying that it does not break its consumers
without knowing who they are. Contract tests catch FM8 (Schema/Contract
Violation) before deployment, not after.
End-to-end tests exercise the full system through its user-facing interface. They catch failures that only appear when all components are running together: configuration mismatches, environment-specific bugs, workflow failures. They are slow (seconds to minutes each), flaky (network, timing, external service state), and expensive to maintain. Their value is high for critical user journeys; their cost makes them unsuitable as the primary test layer.
The test pyramid implies: write many unit tests, some integration tests, a few contract tests, very few end-to-end tests. The test iceberg (inverted pyramid) — more end-to-end tests than unit tests — produces the situation described in the introduction: slow, flaky tests that developers avoid.
When to use each layer: - Unit tests: all pure functions, all business rules, all edge cases and error conditions - Integration tests: all database operations, all queue interactions, all external API clients - Contract tests: all service-to-service interfaces, all public API endpoints - End-to-end tests: critical user journeys (sign up, purchase, payment), smoke tests for deployment verification