The Computing Series

Exercises

Level 1 — Understand

1. Describe the test pyramid: what are the four layers, and what is the intended proportion of tests at each layer?

2. What is the difference between a unit test and an integration test? What kind of bug can an integration test catch that a unit test cannot?

3. What are consumer-driven contract tests, and which failure mode do they exist to prevent before deployment?

Level 2 — Apply

  1. Classify each of the following tests into the correct pyramid layer and justify: (a) a test that creates a Money object and verifies that add(Money(10), Money(5)) returns Money(15); (b) a test that calls GET /api/orders/123 against a running service and checks the HTTP status is 200; (c) a test that inserts a row into a test database through a UserRepository and queries it back; (d) a test that opens a browser, logs in, adds a product to a cart, and checks out.

  2. A team’s CI pipeline takes 38 minutes. Analyse this breakdown: unit tests 200, runtime 2 minutes; integration tests 150, runtime 18 minutes; end-to-end tests 80, runtime 18 minutes. Propose three changes to bring the total under 10 minutes without removing coverage. Justify each change using the pyramid.

  3. An orders service changes its GET /orders/:id response to rename total to totalAmount. There are no contract tests. List all the downstream systems that might break, describe when each would discover the breakage, and calculate the total time-to-detection if deployment to production takes 2 hours.

Level 3 — Design

  1. Design a complete testing strategy for a payment processing service. The service receives payment requests over HTTP, validates them, calls an external card network (Visa/Mastercard), stores results in PostgreSQL, and publishes events to Kafka. Define: which behaviours are covered at each pyramid layer, what test doubles are used and where, how contract tests are structured between this service and its consumers, and what end-to-end tests cover. Estimate test counts and CI runtime.

A complete answer will: (1) define behaviour coverage per layer: unit tests cover validation logic (invalid card numbers, missing fields, amount limits) using no test doubles — these are pure functions; integration tests cover the database interaction (PostgreSQL writes and reads) using a real test database, not a mock; integration tests for the card network use a stub server that returns configurable responses; integration tests for Kafka use an embedded Kafka or a real Kafka container, (2) specify which test doubles are used where: a stub for the card network (controls response to test decline, timeout, and approval scenarios without calling a real network); a spy or recording mock for Kafka (verifies that the correct event payload is published after a successful charge); no mocks for PostgreSQL (a real database gives higher confidence and avoids mock-object drift), (3) design contract tests using Pact: the payment service is the provider; its consumers (order service, notification service) define consumer-driven contracts specifying which fields they read from the PaymentProcessed event schema — the payment service’s CI runs Pact verification to confirm the published event matches all consumer contracts before deployment, and (4) scope end-to-end tests narrowly: cover only the two highest-risk happy-path scenarios (successful payment and declined payment) using a real staging environment — estimate test counts: ~50 unit tests (fast, < 1s total), ~20 integration tests (2–5 minutes), 2 end-to-end tests (5–10 minutes); total CI runtime < 15 minutes.

  1. A team is building a distributed system with six services. They have no contract tests and are considering implementing Pact. Argue for and against implementing Pact: what is the ongoing maintenance cost, what schemas are protected, under what team size and coordination cost does Pact start paying off, and what alternative (if any) provides similar protection with less process overhead?

A complete answer will: (1) make the argument for Pact: in a six-service system, API contract breaks are the primary source of integration failures — a provider team that changes an event schema without notifying consumers causes silent runtime failures (FM8 silent semantic drift); Pact makes contracts explicit and catches breaks in CI before deployment, eliminating a class of production incidents, (2) make the argument against: Pact requires ongoing maintenance — every consumer must update its contract when it legitimately adds a new field; every provider must run Pact verification in its CI pipeline; for a team of 6 engineers (one per service), the overhead of maintaining 6 × 5 = 30 potential consumer-provider contracts may exceed the value of the protection, (3) state the break-even conditions: Pact pays off when services are owned by separate teams (different release cycles, no shared planning), the API surface is large (many fields that consumers depend on), and integration incidents are frequent enough to justify the maintenance burden — below 3 services or for a monorepo with coordinated releases, the break-even rarely occurs, and (4) propose an alternative: schema validation using a shared schema registry (e.g., Apache Avro with a Confluent Schema Registry, or JSON Schema validation on both publish and consume sides) protects against schema breaks with less process overhead than Pact — it does not require consumer teams to define explicit contracts, but it does require both sides to validate against a common schema and it catches incompatible schema changes automatically.

Read in the book →