Test Levels and the Test Pyramid Questions
How unit, integration, component, end-to-end and contract tests fit together and where each provides the most value. Covers the test pyramid and the competing shapes proposed against it (the testing trophy and the honeycomb), multi-layer test architecture, contract testing as the seam between services, choosing the right level to catch a given class of defect cheaply, and what to run per commit versus per release. Includes the cost and confidence trade-offs between fast low-level tests and slower, broader system tests. The scope is which level a test belongs at and why. Deciding how much to invest in testing and where to prioritize under time pressure is covered separately.
Compare and contrast the classical test pyramid with the 'testing trophy' concept and other alternative testing models. Explain the trade-offs between them, and give three concrete production scenarios where deviating from a strict pyramid (favoring more integration or end-to-end tests) makes sense. Include the risks each scenario introduces and how you would mitigate them.
Sample Answer
The classical test pyramid says most tests should be unit tests, fewer should be integration tests, and very few should be end-to-end tests, on the assumption that most risk lives in isolated logic. The testing trophy (associated with Kent C. Dodds) inverts that emphasis for a different class of system: it keeps a small unit-test base, but makes integration tests the LARGEST layer, on the argument that "the more your tests resemble how the software is actually used, the more confidence they give you," and a pure unit test that mocks everything often resembles real usage the least. A related shape, sometimes called the honeycomb (associated with Spotify's microservices testing writeup), similarly shrinks the unit layer and grows the middle layer specifically for service-heavy backends, on the reasoning that a small microservice's real complexity is almost entirely in how it talks to its neighbors, not in isolated internal logic.
The trade-off between the models
Both alternative models trade some unit-test speed and precision for tests that more closely resemble real usage and therefore catch a class of bug (real interaction failures) that heavily-mocked unit tests structurally cannot. The cost is that integration-heavy tests are slower and can be harder to debug when they fail, since a failure could originate in either side of the interaction being tested, and you lose some of the pure pyramid's clean bug-to-test correlation.
Three scenarios where deviating from a strict pyramid makes sense
- A frontend component library where the real risk is composition, not isolated logic. Testing individual components in isolation with heavily mocked props tells you little about whether they actually work together on a real page; integration-style tests that render a realistic tree of components and simulate real user interaction (the trophy's core argument) catch the bugs that matter, at some cost in speed. Risk: slower test runs and less precise failure localization. Mitigation: still keep a lean unit-test layer for pure logic (formatters, validators) where isolation genuinely helps, and reserve the larger integration layer for component composition specifically.
- A small microservice whose logic is thin and whose risk is almost entirely in its contracts with neighbors. A strict pyramid would still demand a large unit-test base even though there is little logic to test in isolation, wasting effort; a honeycomb shape that invests more heavily in contract and integration tests reflects where the actual risk sits. Risk: contract drift between services can slip through if the integration/contract layer isn't kept current with real provider behavior. Mitigation: pair the heavier integration layer with automated, CI-enforced contract verification rather than hand-maintained fixtures.
- A legacy system with tangled, hard-to-unit-test code and existing integration coverage. Rewriting for unit-testability before adding any coverage at all can take months, during which the system ships with no safety net; leaning temporarily on integration or characterization tests around the existing behavior gives real protection sooner. Risk: those tests are slower and give less precise failure information, becoming a long-term crutch if never followed by proper unit-level refactoring. Mitigation: treat the integration-heavy phase as explicitly temporary, with a tracked follow-up plan to extract unit-testable logic once coverage exists to refactor safely.
Trade-offs and pitfalls
The risk in adopting either alternative model is doing so out of preference rather than evidence: the trophy and honeycomb are correct responses to specific risk profiles (interaction-heavy frontends, thin microservices), not universal replacements for the pyramid. Applying a trophy shape to a computation-heavy backend service, where the real risk genuinely is isolated logic, would slow the suite down for no corresponding gain in the bugs it catches.
Many teams cite a '70/20/10' (unit/integration/end-to-end) or similar rule-of-thumb ratio for test distribution. Explain the rationale behind such ratios and the assumptions they make, and describe concrete scenarios where you would deviate from this guideline and why.
Sample Answer
A "70/20/10" (or similarly-shaped) ratio is a useful DEFAULT, not a target to hit for its own sake: it encodes the assumption that most of a typical system's defect risk lives in logic a unit test can isolate cheaply, a smaller amount lives in how components wire together, and only a small remainder needs the expensive proof that the whole assembled system works.
What the ratio assumes
- Most bugs are logic bugs, not integration bugs. If your system's core complexity is business logic (pricing rules, calculations, state machines), this holds well, and a heavy unit-test base pays off directly.
- Integration points are relatively few and stable. The ratio assumes there aren't so many service-to-service or component-to-component seams that integration testing alone would need to be a much larger share to give adequate confidence.
- The team can afford SOME slow, broad tests, but not many. The "10%" isn't zero: it assumes a small curated end-to-end layer is enough to catch whole-system wiring problems, which is only true if your riskiest journeys are few in number.
- Cost scales the way the model assumes. The whole justification for weighting the base so heavily rests on unit tests being drastically cheaper than integration and end-to-end tests; if that cost gap narrows (fast, hermetic integration tests via lightweight containers, for instance - hermetic meaning self-contained: no real network calls or shared external state, so the same test run always gets the same result), the "right" ratio shifts too.
When to deviate, and why
- A thin orchestration service whose logic is almost entirely "call service A, then call service B" has very little unit-testable logic of its own; here the risk genuinely concentrates at the integration boundary, so a heavier integration-test share (closer to something like 40/50/10) reflects reality better than forcing a 70% unit-test floor onto code that barely has any unit-testable branches.
- A frontend-heavy, interaction-driven product where most of the risk is "does clicking through this actually work for a user" may reasonably lean toward more integration-style component tests (a component test renders one UI component together with its real child components but fakes the network or backend, which is what separates it from a unit test, which isolates everything) - closer to the testing-trophy shape (an alternative to the pyramid that keeps a small unit-test base but makes these broader, more realistic tests the largest layer) - than a strict 70/20/10 pyramid, because the thing most likely to break is how components interact on screen, not isolated pure functions.
- A system with very few, very high-stakes end-to-end journeys (payment settlement, safety-critical workflows) may justify a larger-than-10% end-to-end share for those specific journeys, even while the rest of the system keeps the standard ratio, because the cost of an undetected wiring bug there is disproportionately high.
Trade-offs and pitfalls
The most common misuse of this heuristic is treating the numbers as a scorecard: chasing "70% unit tests" by writing large numbers of low-value unit tests for trivial getters, while under-investing in the harder work of a few well-chosen integration and end-to-end tests for the journeys that actually carry business risk. The ratio should be a lagging description of where your test investment naturally lands once you've tested the RIGHT things at each level, not a quota to satisfy directly.
Why should a test suite typically contain far more unit tests than end-to-end tests? Give at least five reasons, spanning both technical factors (such as cost and feedback speed) and organizational factors (such as maintainability), and illustrate each reason with a short concrete example.
Sample Answer
A test suite should contain far more unit tests than end-to-end tests because the two levels trade off cost against realism in opposite directions, and the cheap-and-precise end of that trade dominates for the vast majority of the bugs you actually need to catch.
Five reasons, each with a concrete illustration
- Cost of running. A unit test for a pure function runs in well under a millisecond and needs no external process. An end-to-end test for the same behavior needs a running server, a database, and often a browser or HTTP client, and commonly takes seconds. Run a suite of 2,000 tests: at unit-test speed that finishes in well under a minute; at end-to-end speed the same count could take hours, which no team can afford to run on every commit.
- Feedback speed. A developer who breaks a function wants to know within seconds, while still holding the change in their head. A unit test gives that immediately; an end-to-end test, queued behind environment provisioning and a full pipeline run, might report the failure twenty minutes later, by which point the developer has moved on to something else and the context-switch cost to fix it is much higher.
- Maintainability. A unit test breaks only when the specific function's contract changes. An end-to-end test walks through many screens or endpoints, so it is coupled to all of them at once: a single unrelated UI change (a renamed button, a reordered field) can break dozens of end-to-end tests that were never testing that button in the first place, creating maintenance work with zero corresponding increase in confidence.
- Diagnostic precision (technical). When a unit test fails, the failure message names the exact function and assertion that broke. When an end-to-end test fails, you know only that somewhere across dozens of components something is wrong, and someone has to spend real time narrowing that down, effectively re-deriving the information a unit test would have handed you directly.
- Organizational scaling. As a codebase and team grow, the number of possible logic paths grows roughly with the code, while the number of realistic whole-system journeys grows much more slowly (most new logic is a variation inside an existing journey, not a brand-new one). Unit tests scale naturally with that logic growth; trying to scale end-to-end tests at the same rate produces a suite that is mostly redundant coverage of the same few journeys, wasting CI time without adding proportional confidence.
Trade-offs and pitfalls
None of this means end-to-end tests are dispensable: they are the only level that proves the pieces are actually wired together correctly for a real user, which is exactly the class of bug the other four reasons cannot catch. The pitfall is treating "more unit tests" as license to skip end-to-end coverage of your critical paths entirely; the healthy pattern is a small, curated set of end-to-end tests covering the handful of journeys that matter most (checkout, login), backed by a much larger base of fast unit tests covering the underlying logic.
You are testing a RESTful web application built from a React single-page application, a Node.js REST API, a PostgreSQL database, and an external payment gateway. For each test-pyramid tier (unit, integration, end-to-end), list four concrete example tests you would create, name a common tool or library for each example, and justify why each test belongs at that tier: what it verifies, and what it depends on.
Sample Answer
For a RESTful web application (React SPA, Node.js REST API, PostgreSQL, external payment gateway), each pyramid tier should own a different, non-overlapping slice of confidence, and the tests below make that concrete.
Unit tier (four examples)
- Discount/price calculator: a pure function
calculateDiscount(price, tier); verifies core business math, e.g. Jest for the frontend or a plain test runner on the backend. - React component render logic: does the checkout form component render a validation error when the card field is empty; React Testing Library.
- Request validator: does the order-creation handler reject a negative price before touching the database; a plain Jest unit test with mocked input, no HTTP or DB involved.
- Payment-gateway response parser: given a sample JSON response from the gateway, does your parser extract the correct transaction ID and status; a pure-function Jest unit test with a hard-coded fixture, no real network call.
Each of these verifies one piece of logic in isolation and depends on nothing external, which is why they can run in milliseconds.
Integration tier (four examples)
- API-to-database write path: POST an order to the real Node API running against a real (test) PostgreSQL instance, then query the database directly to confirm the row and its computed total are correct; Supertest plus a real Postgres test container.
- Repository layer against Postgres: an ORM query (e.g., a Prisma or TypeORM query) that joins orders and customers, run against a seeded test database via Testcontainers, to catch a wrong join or a migration mismatch a mocked-DB unit test would miss.
- Payment-gateway client against the gateway's sandbox: using an HTTP client library such as Axios (or the gateway's official Node.js SDK if one is provided), call the gateway's real sandbox endpoint (not your parser in isolation) to confirm your client sends a well-formed request and correctly handles the sandbox's real success and decline responses.
- React SPA against a mocked API layer: render the checkout page and confirm it calls the real API client code (not the component logic alone) and correctly updates state on a real HTTP response, using a tool like MSW (Mock Service Worker) to intercept only the network boundary, not the application code.
Each of these proves two real components agree on a contract (route shape, SQL schema, gateway request format) that a unit test, by construction, cannot check because it never invokes the second component for real.
End-to-end tier (four examples)
- Full checkout journey: drive the real React SPA in a real browser through add-to-cart, checkout form, and payment, against the real (or sandboxed) full stack, using Playwright or Cypress, to prove the whole assembled system delivers a working checkout.
- Payment failure path end-to-end: using Playwright or Cypress, submit a card the gateway's sandbox is configured to decline, and confirm the SPA shows the correct user-facing error, proving the failure path is wired correctly all the way through, not just handled by the parser in isolation.
- Session and auth flow: using Playwright or Cypress, log in, add an item, refresh the page, and confirm the cart persists, exercising the real session/cookie mechanism no lower-level test touches.
- Order confirmation and receipt: using Playwright or Cypress to complete the purchase, combined with a test email-capture tool such as Mailhog or Mailtrap, confirm a confirmation email or receipt page reflects the correct final total, proving the pricing logic, the database write, and the presentation layer all agree once wired together for real.
Why each test belongs where it does
The dividing line is what would have to be REAL for the test to fail the way it's meant to: the unit tests fail only if the pure logic is wrong; the integration tests fail only if two real components disagree, even when each one's internal logic is correct in isolation; the end-to-end tests fail only if something in the full assembly, including things no lower test can see (routing, session state, real gateway behavior), is broken.
Trade-offs and pitfalls
The most common mistake with this stack specifically is testing the payment-gateway integration primarily at the end-to-end level because "it's the riskiest part": that inflates the slowest, flakiest tier with coverage that a much cheaper integration test against the gateway's sandbox could provide almost as well. Reserve end-to-end for the few journeys where the VALUE is specifically in proving the pieces are wired together, and push everything else down a tier.
Discuss the trade-offs and return on investment between automating end-to-end UI tests versus API-level tests. Consider maintainability, flakiness, speed, coverage, and debugging ease, and how each should be used in release gating versus production monitoring. Conclude with a pragmatic hybrid approach and rules of thumb for deciding which user stories to automate at the UI level versus the API level.
Sample Answer
End-to-end UI tests and API-level tests both exercise real, wired-together behavior, but they trade realism against cost in opposite directions, and the right answer for most teams is not "pick one" but a deliberate hybrid.
The trade-offs, dimension by dimension
- Maintainability. UI tests are coupled to the rendered page: a renamed button, a reordered form field, or a redesigned layout can break many UI tests that were never testing that element, producing maintenance work disproportionate to any real regression. API tests are coupled only to the API contract, which typically changes far less often than the UI, so they need less ongoing repair.
- Flakiness. UI tests depend on rendering timing, animations, and browser quirks, all classic sources of non-determinism; API tests, hitting a server directly with no rendering step, are inherently more deterministic and far less prone to intermittent failure.
- Speed. API tests skip the browser entirely, so they typically run several times faster than the equivalent UI test, which matters directly for how large a suite you can afford to run per pull request.
- Coverage. A UI test is the only one of the two that can prove the interface itself renders correctly and reacts to real interaction; an API test proves the underlying business logic and data flow are correct but says nothing about whether a real user sees the right thing on screen.
- Debugging ease. When an API test fails, the failure usually points precisely at a request/response mismatch; when a UI test fails, you must first determine whether the underlying logic is wrong, the API contract changed, or the UI simply rendered slower than the test expected, which takes more investigation time per failure.
How each should be used in release gating versus monitoring
API tests are cheap and reliable enough to gate a release directly: a failing API test is a strong, low-noise signal that something is genuinely broken, so blocking on it costs little and catches real problems. UI tests, being slower and flakier, are better used more sparingly as a release gate (a small, curated smoke set for your most critical journeys) and more heavily as ongoing production monitoring (synthetic UI checks running continuously against production, alerting on failure) where an occasional false alarm is a minor cost rather than a blocked release.
A pragmatic hybrid approach and rules of thumb
Default new coverage to the API level; only add a UI test when the thing you're actually verifying is specifically about rendering or interaction (does a validation message appear where expected, does a button visibly disable during submission) rather than about the underlying business outcome (does the order total compute correctly), which an API test can verify just as well at a fraction of the cost. As a rule of thumb: if you could rewrite a proposed UI test to call the API directly and it would still prove the same business assertion, it belongs at the API level; if rewriting it that way would lose the actual thing being tested, it belongs at the UI level.
Trade-offs and pitfalls
The trap in a hybrid strategy is inconsistency: without an explicit rule like the one above, teams tend to add UI tests reflexively because they feel more "complete," slowly re-accumulating exactly the maintenance and flakiness burden the hybrid approach was meant to avoid. Make the API-first default explicit and require a stated reason (specifically about rendering or interaction) to add a UI test instead.
Unlock Full Question Bank
Get access to all 20 Test Levels and the Test Pyramid interview questions and detailed answers.
Sign in to ContinueJoin thousands of developers preparing for their dream job.