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.
A production bug in a critical API path slipped through despite your integration tests passing. Analyze the possible weaknesses across test-pyramid levels, environment parity, test selection, and CI gating that could explain how this happened, and propose a concrete set of improvements and guardrails to prevent similar escapes.
Sample Answer
Integration tests passing while a bug still reaches production tells you the bug lives in a gap the integration suite structurally cannot see, and the diagnosis needs to check four distinct places, not just "add more tests."
Weaknesses across pyramid levels
The bug might be a pure logic error that a unit test would catch far more precisely than an integration test ever could; if no unit test exists for the function that actually contains the bug, the integration test that exercises it indirectly may pass just by luck, testing a code path that happens not to trigger the specific edge case. Alternatively, the bug might be something ONLY an end-to-end test can see, such as a UI or client-side issue in how a correct API response gets rendered or handled, which no amount of API-level integration testing would ever exercise.
Environment parity
Integration tests commonly run against a test database or test configuration that differs from production in ways that matter: different data volume (a query that's fast on a small test dataset but times out on production scale), different configuration (a feature flag or environment variable set differently), or a downstream dependency's test double behaving more forgivingly than the real production service does. Any of these can produce a passing integration test that tells you nothing about production behavior.
Test selection
If the CI pipeline uses test-impact analysis or tagging to run only a subset of tests per change (to keep PR feedback fast), an imprecise dependency map can silently skip a test that would have caught this specific bug, because the tooling didn't correctly recognize that the changed code affected that test's path. This is invisible in the CI output, since the skipped test doesn't fail, it simply never runs.
CI gating
Even if the right test exists and would have failed, a gating policy gap can let a bug through anyway: for example, if a specific integration test is in a "monitored but non-blocking" tier (perhaps because it was historically flaky and got demoted), its failure might have been logged but not treated as a merge blocker, and the team missed the signal.
Concrete improvements and guardrails
- Once the specific missing coverage is identified, add a UNIT test for the exact logic bug first (fastest, most precise regression protection), not just another integration test, unless the bug is genuinely about wiring rather than logic.
- Audit environment parity specifically for the dimension that caused this bug (data volume, config, a lenient test double) and either close that gap or add an explicit test that exercises the production-like condition.
- If test selection is in use, audit whether its dependency map correctly captured this bug's code path, and tighten or add an explicit tag if the automated mapping missed it.
- Review the gating policy for any test tier that's "monitored but non-blocking" and confirm each one is there by a deliberate, current decision rather than institutional inertia from a past flakiness problem.
Trade-offs and pitfalls
The instinctive response to an escaped bug is "add a test for exactly this case," which is necessary but insufficient if the root cause is one of the systemic gaps above (environment parity, test selection, or gating): a single new test closes the specific hole discovered this time but leaves the same category of bug able to escape again through the same systemic gap. Treat the specific bug as a symptom that should prompt an audit of the four areas above, not just a checklist item to close.
Describe the test pyramid and how you would apply it to a modern single-page-application stack (React frontend, Node API, PostgreSQL database). For each layer (unit, integration/component, and end-to-end), give concrete examples of what to test and recommended tooling, propose an approximate test-count ratio across the layers, and describe how you would validate and adjust that ratio over time as the product matures.
Sample Answer
For a React-frontend, Node-API, PostgreSQL-database SPA stack, the pyramid maps onto three layers whose boundary follows the technology seam as much as the logical one.
What to test at each layer, with tooling
- Unit: pure functions and isolated logic on both sides of the stack, for example a price-formatting helper or a validation function on the frontend, and a business-rule function on the Node API. Recommended tooling: Jest (or Vitest) for both the React frontend and the Node backend, since a single test runner across the stack keeps tooling simple.
- Integration/component: on the frontend, rendering a React component with React Testing Library and confirming it correctly calls a mocked API client and updates its own state and DOM in response, which proves the component's own logic and rendering without needing the real backend running; on the backend, hitting the real Node API with Supertest against a real (test) PostgreSQL database, proving the route, the query, and the schema all agree, which no frontend-only or backend-only unit test can show.
- End-to-end: driving the real React app in a real browser against the real API and database (or a close staging equivalent) using Playwright or Cypress, proving the whole assembled stack delivers a correct user-facing outcome, such as a full checkout flow from click to confirmation.
Guidance on test-count ratio
A reasonable starting ratio for this stack is roughly 65-70% unit tests (split across frontend logic and backend logic), 20-25% integration/component tests (split between frontend component tests and backend API-to-database tests), and 5-10% end-to-end tests covering only the handful of journeys where the whole assembled stack matters most (checkout, authentication). The SPA's heavy client-side interaction pushes the integration/component share slightly higher than a pure backend service would need, since a meaningful share of this stack's real risk lives in how React components manage state and respond to user interaction, which a backend-only pyramid wouldn't need to account for.
Validating and adjusting the ratio over time
Track, per release, which layer actually caught each regression found either in code review, staging, or production, and compare that distribution to your current test-count ratio: if end-to-end tests are catching bugs that a component test could have caught more cheaply, that's a signal to push more coverage down a layer; if production bugs keep slipping through despite full coverage lower in the pyramid, that's a signal the end-to-end layer, not the lower layers, needs to grow for that specific journey. Revisit the ratio on a fixed cadence (quarterly is common) rather than continuously, since a ratio that reacts to every single incident tends to overfit to the most recent bug rather than reflecting the system's actual steady-state risk.
Trade-offs and pitfalls
The most common mistake on this specific stack is testing React component behavior primarily through end-to-end browser tests, because it's the most "realistic," when a React Testing Library component test at the integration/component layer can prove the same interaction logic in a small fraction of the time and with far less flakiness. Reserve full end-to-end coverage for the journeys where the point genuinely is proving the whole stack, frontend, API, and database together, works correctly.
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.
Tell me about a time you discovered a production bug that was caused by missing or insufficient tests. Using the STAR structure, describe the situation, the task you were responsible for, the actions you took to fix the bug and improve the tests or process (including which test level was missing and why), and the measurable result afterward.
Sample Answer
A strong answer to this question names the specific test level that was missing, not just "we didn't have enough tests," because the level tells the interviewer exactly what you learned and whether your fix addressed the real gap.
How to structure the STAR response
Situation: describe the system and the context concisely, for example a checkout service where a promotional-discount feature shipped and, in production, allowed two discount codes to be combined when the business rule required only one to apply at a time.
Task: state your specific responsibility, for example being the engineer or QA owner responsible for the checkout service's test coverage and for triaging the incident once it was reported.
Action: this is the part worth being most concrete about. Two honest, common shapes:
- The bug existed at the UNIT level (the discount-combination rule itself was wrong) but no unit test covered that specific combination of inputs, only the single-discount case; the integration and end-to-end tests that existed happened to use test data that never exercised two codes together, so they passed without ever exercising the buggy path. The fix: add the missing unit test covering the combination case, verify it fails against the buggy code and passes against the fix, and then audit for other similarly-unexercised input combinations in the same rule.
- The bug existed at the INTEGRATION level (each discount's logic was individually correct, but the two didn't compose correctly once wired through the real order-total calculation, a case unit tests of each discount rule in isolation could not see). The fix: add an integration test that exercises the real combined path, not just each rule mocked in isolation.
Either shape is a legitimate, honest answer; the point is naming precisely which level was missing and why the existing suite's blind spot let the bug through, then closing that specific gap rather than adding coverage generically.
Result: describe the outcome concretely but honestly: for example, that specific discount-combination bug did not recur, and the broader audit of the same rule surfaced and closed a small number of similarly-unexercised input combinations before they caused an incident, giving you a real, verifiable before/after data point (the audit finding count) rather than an invented precision metric.
Trade-offs and pitfalls
The common way this answer goes wrong in an interview is staying vague ("we added more tests and it got better"), which gives the interviewer no way to judge your actual technical judgment; naming the specific test level, the specific gap in that level's coverage, and the specific fix is what turns a generic incident story into evidence of real testing judgment. A second pitfall is inventing a precise-sounding improvement metric you didn't actually measure; if you don't have a real number, describe the outcome qualitatively (no recurrence, caught in the audit before shipping) rather than fabricating one.
Explain the differences between smoke tests, regression tests, integration tests, system tests, and user-acceptance tests, and between functional and non-functional testing. For each, describe when it should be executed in a typical CI/CD pipeline and give one concrete example test appropriate for an e-commerce web application.
Sample Answer
These names describe two different axes, not one: smoke, regression, integration, system, and user-acceptance tests describe SCOPE and PURPOSE within a release process, while functional versus non-functional describes WHAT KIND of requirement is being verified. A single test can sit at one point on each axis at once (for example, a load test is a non-functional system test).
The five scope/purpose types
| Type | What it verifies | When it runs in CI/CD | Example for an e-commerce app |
|---|---|---|---|
| Smoke | The absolute basics work at all: the app starts, key pages load, nothing is catastrophically broken | Immediately after every deploy, before anything else runs | Confirm the homepage and checkout page both return HTTP 200 after a deploy |
| Regression | Previously-fixed bugs and previously-working behavior haven't broken again | On every pull request, or nightly for the full suite | Re-run the specific test that reproduces a past bug where applying two discount codes together double-discounted an order |
| Integration | Two or more real components agree on how they interact | Pull request / merge | Confirm the checkout API correctly writes a new order row to the real database |
| System | The whole assembled application behaves correctly as one unit against requirements | Pre-release, in a staging-like environment | Walk through browsing, adding to cart, and completing checkout as one continuous validation of the whole system, not just one flow |
| User-acceptance | The system satisfies what the business or the customer actually asked for | Just before release, often with a human sign-off | A product owner or customer confirms that the new "buy now, pay later" option behaves the way they specified in the requirements |
Regression testing's specific effect on release velocity
A solid regression suite is what lets a team ship frequently without re-manually-verifying everything that already worked: automated regression tests reliably catch a bug like the double-discount example above, where a change to one part of the pricing logic silently breaks a previously-correct interaction, the moment it's introduced, rather than after a customer reports it. What automated regression tests do NOT reliably catch is a bug that requires actual human judgment to notice, such as a new promotional banner rendering with confusing or misleading wording, which passes every automated check while still being wrong; that class of issue needs manual exploratory testing precisely because "correctness" here is a judgment call, not a fixed assertion.
Keeping a growing regression suite fast and reliable
As a regression suite grows, two problems compound: it gets slower, and it accumulates flaky tests (ones that fail intermittently for reasons unrelated to real regressions). Keep it fast by running only the subset of regression tests relevant to changed code on every PR, reserving the full suite for a nightly run. Keep it reliable by treating a flaky regression test as a bug in the test itself, not background noise to tolerate: track a rerun rate per test, and either fix or quarantine (temporarily exclude with an owner assigned to repair it) any test whose failures don't correlate with real code changes, since an ignored flaky test trains the team to distrust the whole suite.
Functional versus non-functional testing, as a separate axis
Functional testing asks "does the checkout flow correctly compute the total and complete the order," a direct check against a stated feature requirement. Non-functional testing asks a different kind of question entirely: for the same checkout flow, does it perform well under load (performance), does it protect payment data appropriately (security), is it usable by someone unfamiliar with the site (usability), and can someone using a screen reader complete a purchase (accessibility). These four non-functional concerns should be prioritized before release based on business risk, not treated as equally weighted: for a payment flow specifically, security and performance under peak load typically deserve the most pre-release attention, since a failure there has the most severe and hardest-to-reverse consequences, while usability and accessibility issues, though real and important, are more often caught and improved iteratively after release without the same acute risk.
In a typical CI/CD pipeline, functional tests run continuously as part of the regular suite on every commit or pull request, since a functional regression, like the checkout total being computed incorrectly, is valuable to catch immediately. Non-functional tests usually run on a slower, scheduled cadence: a load test simulating peak Black Friday traffic against the checkout API is a concrete non-functional example, and it typically runs nightly or pre-release rather than blocking every commit, since it needs a longer, resource-heavy run that would slow down PR feedback if it gated every merge.
Trade-offs and pitfalls
The most common confusion is treating "system test" and "end-to-end test" as interchangeable; they overlap heavily in practice but system testing traditionally emphasizes validating the WHOLE application against its requirements as one unit (often owned by QA, closer to release), while end-to-end testing more narrowly emphasizes a specific user JOURNEY through the real stack (often automated and run continuously). Naming this distinction explicitly, rather than treating the terms as synonyms, is itself a signal of depth in this space.
Unlock Full Question Bank
Get access to all 22 Test Levels and the Test Pyramid interview questions and detailed answers.
Sign in to ContinueJoin thousands of developers preparing for their dream job.