Skip to content

Testing Strategy#

Problem statement (interviewer prompt)

Design the test strategy for a microservice doing critical financial work. Decide what goes in unit, component, integration, contract, E2E, and load tests. Set a flake budget, choose test doubles (fakes / stubs / mocks), and explain TDD's place in the workflow.

Concept illustration
flowchart TB
  E2E[E2E - browser, real stack<br/>few]
  INT[Integration<br/>some]
  UNIT[Unit<br/>many]
  UNIT --> INT --> E2E

  classDef u fill:#d1fae5,stroke:#065f46,stroke-width:1px,color:#0f172a;
  classDef i fill:#fef3c7,stroke:#92400e,stroke-width:1px,color:#0f172a;
  classDef e fill:#fee2e2,stroke:#991b1b,stroke-width:1px,color:#0f172a;
  class UNIT u;
  class INT i;
  class E2E e;

    classDef client fill:#dbeafe,stroke:#1e40af,stroke-width:1px,color:#0f172a;
    classDef edge fill:#cffafe,stroke:#0e7490,stroke-width:1px,color:#0f172a;
    classDef service fill:#fef3c7,stroke:#92400e,stroke-width:1px,color:#0f172a;
    classDef datastore fill:#fee2e2,stroke:#991b1b,stroke-width:1px,color:#0f172a;
    classDef cache fill:#fed7aa,stroke:#9a3412,stroke-width:1px,color:#0f172a;
    classDef queue fill:#ede9fe,stroke:#5b21b6,stroke-width:1px,color:#0f172a;
    classDef compute fill:#d1fae5,stroke:#065f46,stroke-width:1px,color:#0f172a;
    classDef storage fill:#e5e7eb,stroke:#374151,stroke-width:1px,color:#0f172a;
    classDef external fill:#fce7f3,stroke:#9d174d,stroke-width:1px,color:#0f172a;
    classDef obs fill:#f3e8ff,stroke:#6b21a8,stroke-width:1px,color:#0f172a;
    class E2E client;
    class INT,UNIT service;

The test pyramid: many fast unit tests at the base, fewer integration tests in the middle, a handful of end-to-end at the top. The opposite shape (the "ice-cream cone") is what slow, flaky test suites look like.

Layers#

Layer What it covers Speed Brittleness
Unit one function / class in isolation ms low
Component one service end-to-end in-process tens of ms low
Integration service + real DB / queue / cache hundreds of ms medium
Contract API consumer ↔ provider expectations ms low
E2E browser / mobile through whole stack seconds-minutes high
Performance / load latency, throughput, soak varies n/a
Chaos failure injection varies n/a

Test doubles#

classDiagram
  class Dummy {
    +never used
  }
  class Stub {
    +returns canned data
  }
  class Fake {
    +real-ish working impl<br/>e.g. in-memory DB
  }
  class Spy {
    +records calls
  }
  class Mock {
    +pre-programmed expectations<br/>verifies usage
  }
Term Behaviour Verifies usage? Common library
Dummy filler argument no n/a
Stub returns fixed values no hand-coded
Spy records calls, default behaviour optional Mockito spy, jest
Mock expectations programmed up-front yes Mockito mock, jest.mock
Fake in-memory working impl no hand-coded (in-mem repo)

Rule of thumb: prefer fakes, fall back to stubs, use mocks sparingly.

TDD cycle#

flowchart LR
  R([Red - write failing test])
  G([Green - write minimum code to pass])
  REF([Refactor - clean up, keep green])
  R --> G --> REF --> R

    classDef client fill:#dbeafe,stroke:#1e40af,stroke-width:1px,color:#0f172a;
    classDef edge fill:#cffafe,stroke:#0e7490,stroke-width:1px,color:#0f172a;
    classDef service fill:#fef3c7,stroke:#92400e,stroke-width:1px,color:#0f172a;
    classDef datastore fill:#fee2e2,stroke:#991b1b,stroke-width:1px,color:#0f172a;
    classDef cache fill:#fed7aa,stroke:#9a3412,stroke-width:1px,color:#0f172a;
    classDef queue fill:#ede9fe,stroke:#5b21b6,stroke-width:1px,color:#0f172a;
    classDef compute fill:#d1fae5,stroke:#065f46,stroke-width:1px,color:#0f172a;
    classDef storage fill:#e5e7eb,stroke:#374151,stroke-width:1px,color:#0f172a;
    classDef external fill:#fce7f3,stroke:#9d174d,stroke-width:1px,color:#0f172a;
    classDef obs fill:#f3e8ff,stroke:#6b21a8,stroke-width:1px,color:#0f172a;
    class R,G,REF service;

Useful for: - New behaviour with a clear spec. - Bug-fix (write failing test first). - Algorithms with well-defined inputs/outputs.

Not useful for: - Exploratory UI work. - Spike code expected to be thrown away.

Contract testing#

flowchart LR
  CC[Consumer test]
  PACT[Pact file - JSON contract]
  PV[Provider verification]
  CC --> PACT --> PV

    classDef client fill:#dbeafe,stroke:#1e40af,stroke-width:1px,color:#0f172a;
    classDef edge fill:#cffafe,stroke:#0e7490,stroke-width:1px,color:#0f172a;
    classDef service fill:#fef3c7,stroke:#92400e,stroke-width:1px,color:#0f172a;
    classDef datastore fill:#fee2e2,stroke:#991b1b,stroke-width:1px,color:#0f172a;
    classDef cache fill:#fed7aa,stroke:#9a3412,stroke-width:1px,color:#0f172a;
    classDef queue fill:#ede9fe,stroke:#5b21b6,stroke-width:1px,color:#0f172a;
    classDef compute fill:#d1fae5,stroke:#065f46,stroke-width:1px,color:#0f172a;
    classDef storage fill:#e5e7eb,stroke:#374151,stroke-width:1px,color:#0f172a;
    classDef external fill:#fce7f3,stroke:#9d174d,stroke-width:1px,color:#0f172a;
    classDef obs fill:#f3e8ff,stroke:#6b21a8,stroke-width:1px,color:#0f172a;
    class CC,PACT,PV service;

Pact / Spring Cloud Contract: consumer writes "I expect provider to respond like this"; provider runs the spec against its real handler. Catches API drift without coupling deploys.

End-to-end tests (judiciously)#

  • Cypress / Playwright / Selenium for browsers.
  • Detox / Appium for mobile.
  • One smoke test per critical user journey (login, checkout, send-message).
  • Run on every deploy; don't run on every PR (too slow).

Performance testing#

  • Load test: target QPS, sustained, measure p99.
  • Stress test: increase until failure; observe where + how.
  • Soak test: 24 h+ at production rate; find leaks.
  • Spike test: sudden bursts; verify autoscale + protection.

Tools: k6, Locust, Gatling, wrk, vegeta.

Coverage#

  • 80% line coverage is a floor, not a goal.
  • Branch coverage > line coverage in usefulness.
  • 100% is over-engineering; focus on the code path that matters.

Property-based testing#

prop_reverseTwice :: [Int] -> Bool
prop_reverseTwice xs = reverse (reverse xs) == xs

Generates many random inputs and asserts an invariant. Hypothesis (Python), fast-check (JS), jqwik (Java), QuickCheck (Haskell).

Mutation testing#

Run "mutants" of your code (flip > to <, etc.); a useful suite catches them. Tools: PITest (Java), Stryker (JS).

Anti-patterns#

  • Testing the framework: don't test that Spring autowires.
  • Mocking everything: brittle, low-value tests pass while the system is broken.
  • One giant integration test: long, hard to debug, slow.
  • Tests with sleeps: use awaitility / polling assertions.
  • Snapshot tests as the only safety net: large diffs become rubber-stamped.

Glossary & fundamentals#

Concepts referenced in this design. Each row links to its canonical page; the tag column shows whether it is a high-level (HLD) or low-level (LLD) concept.

Tag Concept What it is Page
LLD Testing strategy pyramid, doubles, TDD, contracts testing-strategy
LLD Behavioural patterns Strategy, Observer, State, Command, Chain behavioral-patterns

Quick reference#

Speed targets#

  • Unit suite: < 30 s on local laptop.
  • Component suite: < 2 min.
  • Integration suite (per service): < 5 min.
  • E2E smoke: < 10 min.

Flake budget#

  • Aim for < 0.1% flake rate.
  • Quarantine flaky tests; either fix in 1 week or delete.

Test data#

  • Each test seeds its own fixtures; never depend on global state.
  • Use builders or factories (UserBuilder.aUser().withEmail(...)).
  • Teardown via transaction rollback (faster than DELETE).

CI strategy#

  • Pyramid bottom on every PR.
  • Pyramid top on merge to main + nightly.
  • Performance + chaos on a schedule, not per-PR.

Refs#

  • Test-Driven Development by Example - Kent Beck.
  • xUnit Test Patterns - Gerard Meszaros.
  • Growing Object-Oriented Software, Guided by Tests - Freeman & Pryce.
  • Martin Fowler: "Test Pyramid", "Test Doubles" articles.
  • Pact docs (pact.io).

FAQ#

What is the test pyramid?#

It is a guideline for test ratios: many fast unit tests at the base, fewer integration tests in the middle, and a few E2E tests on top to validate the full stack.

What is the ice-cream-cone anti-pattern in testing?#

It is the inverted pyramid: lots of slow brittle E2E tests with few unit tests. Builds become long and flaky, slowing the team and eroding trust in the suite.

What is the difference between a stub, a mock, and a fake?#

A stub returns canned answers, a mock asserts that calls happened in a certain way, and a fake is a working but lightweight implementation, like an in-memory DB.

When should I use contract testing?#

Use contract tests between services that communicate over the network. They check that producer and consumer agree on the shape without running both end to end.

How fast should unit tests be?#

Each unit test should run in milliseconds. The whole suite should finish in seconds so developers can run it on every save without breaking flow.

  • SOLID Principles: SRP and ISP make units testable; DIP enables test doubles and fakes
  • Dependency Injection: DI frameworks are the primary way to inject mocks, stubs, and fakes in tests
  • Clean Architecture: layered architecture defines the test pyramid boundaries: unit, integration, E2E

Further reading#

Curated, high-credibility sources for going deeper on this topic.