SOFTWARE ARCHITECTURE / SYSTEM CONCEPT BRIEF

Testing pyramid

The testing pyramid is a strategy for balancing automated tests.

BeginnerPhase 08 / Topic 17 of 17RequirementsTrade-offsFailure modes
01

Overview

The testing pyramid is a strategy for balancing automated tests. The wide base is many fast, isolated unit tests of individual functions and classes. The middle layer is fewer integration tests that verify components working together (service plus database, API plus queue). The narrow top is a small number of end-to-end tests that drive the whole system like a user would.

The shape reflects cost and speed. Unit tests run in milliseconds and pinpoint failures; end-to-end tests are slow, brittle, and expensive to maintain but catch problems nothing else can. A healthy suite gives fast feedback on every commit and high confidence before release. The anti-pattern is the 'ice cream cone', with mostly manual and end-to-end tests and few unit tests.

Inspecting a car

Engineers test each part on a bench (unit), then test assembled systems like brakes with the wheel (integration), and finally take a few full test drives (end-to-end). Test drives are valuable but you cannot test every bolt that way.

02

When to use it

  • Designing a test strategy for a service or product.
  • Deciding what to test at which level.
  • Speeding up slow, flaky CI pipelines.
  • Interview discussions of quality in system and LLD designs.
03

Where it shows up in interviews

Test strategy

Recognize it when: how would you test this system?

  • Design testing for a payment service
  • Design CI for a microservices platform
04

Where it is used in real software

Google

Google's testing guidance favors a large proportion of small tests, fewer medium tests, and few large end-to-end tests.

Testcontainers

Integration tests spin up real PostgreSQL, Kafka, or Redis in Docker for realistic but automated checks.

Playwright and Cypress

Used for a small set of critical end-to-end browser journeys such as sign-up and checkout.

05

Key terms

Unit test
Tests one unit in isolation, fast and deterministic.
Integration test
Tests components together with real dependencies such as databases.
End-to-end test
Tests the full system through its external interface.
Contract test
Verifies services agree on API contracts without full end-to-end setup.
Flaky test
Passes and fails nondeterministically.
06

How it works, step by step

  1. 1
    Unit test business logic

    Pure rules, calculations, state transitions, with fakes for dependencies.

  2. 2
    Integration test boundaries

    Repository queries against a real database, message handlers against a real broker.

  3. 3
    Contract test service interfaces

    Consumer-driven contracts catch breaking API changes early.

  4. 4
    End-to-end test critical journeys

    A handful of flows such as sign-up, checkout, and payment.

  5. 5
    Run layers at the right time

    Units on every commit, integration in CI, end-to-end before and after deploys.

The pyramid from base to top
Step 1 / 3
Unit (70%)
Integration (20%)
End-to-end (10%)

STEP 1Unit tests: thousands, milliseconds each. Example: discount calculation, order state transitions.

07

Test levels compared

For a checkout feature

Step 1 / 4
LevelExample testSpeedCatches
UnitPricingEngine applies coupon and tax~1 msLogic bugs
IntegrationOrder saved and read back from PostgreSQL~1 sSQL and mapping bugs
ContractCheckout matches the payment API contract~100 msBreaking API changes
End-to-endUser completes checkout in a browser~30 sWiring and configuration issues

NOWLevel: Unit | Example test: PricingEngine applies coupon and tax | Speed: ~1 ms | Catches: Logic bugs

Push each test to the lowest level that can catch the bug; reserve end-to-end tests for critical paths.

08

Implementation

import { describe, expect, it } from "vitest"; // Unit: pure logic, no I/Odescribe("PricingEngine", () => {  it("applies coupon before tax", () => {    const engine = new PricingEngine({ taxRate: 0.1 });    expect(engine.total([{ priceCents: 1000, qty: 2 }], { coupon: "SAVE10" })).toBe(1980);  });}); // Integration: real database via Testcontainersdescribe("OrderRepository", () => {  it("persists and loads an order", async () => {    const pg = await new PostgreSqlContainer("postgres:16").start();    const repo = new OrderRepository(await connect(pg.getConnectionUri()));    const id = await repo.save({ userId: "u1", totalCents: 1980 });    expect(await repo.find(id)).toMatchObject({ userId: "u1", totalCents: 1980 });    await pg.stop();  });}); // End-to-end: one critical journey (Playwright)// test("checkout", async ({ page }) => {//   await page.goto("/products/42"); await page.click("text=Add to cart");//   await page.click("text=Checkout"); await page.fill("#card", "4242424242424242");//   await page.click("text=Pay"); await expect(page.getByText("Order confirmed")).toBeVisible();// });
09

Complexity and performance

Typical ratio~70 / 20 / 10

Unit / integration / end-to-end, a guideline not a rule.

Feedback time targetMinutes for CI

Parallelize and keep unit tests fast.

10

Trade-offs

Confidence vs speed

End-to-end tests give the most realistic confidence but are slow and flaky; unit tests are fast but can miss integration issues.

Mocks vs real dependencies

Heavy mocking makes tests fast but can test the mocks rather than reality; integration tests with real databases catch mapping and query bugs.

11

Variants and related techniques

Testing trophy

Emphasizes integration tests more heavily, popular for front-end applications.

Testing honeycomb

For microservices, focuses on integration and contract tests over isolated units.

12

Common mistakes

  • Ice cream cone (mostly manual and UI tests).

    Fix: Move checks down to unit and integration levels.

  • Ignoring flaky tests.

    Fix: Quarantine and fix them quickly; flakiness destroys trust in CI.

  • Testing implementation details.

    Fix: Test behavior through public interfaces so refactoring does not break tests.

13

Interview questions

What is the testing pyramid and why that shape?

Many fast unit tests at the base, fewer integration tests in the middle, and a small number of end-to-end tests at the top. Lower levels are faster, cheaper, and more precise, so most coverage lives there; end-to-end tests cover only critical user journeys.

How would you test a payment service?

Unit test pricing, state transitions, and idempotency logic; integration test persistence and message handling with Testcontainers; contract test the payment provider API and downstream consumers; and run a few end-to-end checkout flows against a sandbox, plus canary monitoring in production.

14

Practice problems

ProblemDifficultyWhat it trains
Write a test plan for a URL shortenerEasyChoosing levels.
Reduce a 40-minute flaky CI suiteMediumRebalancing the pyramid.