Skip to content

Interview Prep

Testing & QA Tools Interview Questions

Unit, end-to-end, API and performance testing tools — the ones interviewers actually name and expect you to have used.

20 of 20 Testing & QA Tools questions shown. Answers are hidden by default — try each one before revealing it.

  1. 1.You write this Jest test for a function that fetches a user by id: test("fetches the right user", () => { fetchUser(1).then((user) => { expect(user.name).toBe("Alice"); }); }); fetchUser(1) actually resolves with { name: "Bob" }, so the assertion is false — but the test suite reports this test as passing. Why?

    Medium

    Show answer and explanation

    A Jest test function that returns nothing is treated as synchronous: Jest runs the body, and the moment it returns — here, immediately, since a promise is created but never awaited or returned — Jest records the test as finished. The .then() callback, and the expect() inside it, only run on a later microtask turn, after Jest has already reported a pass. If that assertion throws, it typically surfaces as an unhandled promise rejection warning rather than a failed test. The fix is to return the promise (return fetchUser(1).then(...)) or, more idiomatically, mark the test async and await it, so the test function itself doesn't resolve until the assertion has actually run. The auto-mock option is a tempting-sounding distractor but wrong: Jest does not auto-mock modules unless automock is explicitly configured or jest.mock() is called; expect() inside a callback executes normally, it just runs too late to affect an already-completed test.

    jestasyncpromisesunit-testing

  2. 2.Cypress runs your test code inside the same browser as the application under test, driving it directly through its own architecture, while Selenium sits outside the browser and drives it remotely through the WebDriver protocol. Which of the following is a direct, practical consequence of Cypress's architecture?

    Medium

    Show answer and explanation

    Because Cypress executes inside the browser, in the same run loop as the application, it has excellent access to the DOM, network layer, and app internals, and commands run fast and reliably without the network round-trips a remote WebDriver call needs. The trade-off is that a single Cypress test is tied to one browser tab: it cannot open and juggle two separate tabs or windows within the same test, because doing so would mean controlling a browser context Cypress isn't injected into. Selenium (and Playwright) drive the browser from an outside control channel instead, so neither has that particular constraint and both can manage multiple windows or tabs in one test. The other options are false: Cypress running real JavaScript-heavy SPAs is one of its strongest, most common use cases, not a limitation; Cypress does not reload the page between every command; and both tools support headless and headed execution.

    cypressseleniumtest-architecturee2e-testing

  3. 3.A pytest test module contains this fixture: @pytest.fixture(scope="module") def db_connection(): conn = create_connection() yield conn conn.close() Five test functions in that same module all take db_connection as an argument. How many times does create_connection() run, and when does conn.close() run?

    Medium

    Show answer and explanation

    scope="module" means the fixture is instantiated once per test module, and that single instance is handed to every test function in the module that requests it, rather than being rebuilt for each one. The code after yield is the fixture's teardown, and pytest runs it exactly once, after the last test in the fixture's scope has completed — here, after the fifth and final test in the module, not after each one individually. Option B describes pytest's default scope, "function", where setup and teardown run around every single test. Option C describes scope="session", which shares one instance across every module in the entire run. Option D is simply wrong: yield-based fixtures are exactly how pytest implements setup/teardown pairing, and the code after yield always runs as teardown (even when a test fails, by default) as long as execution actually reached the yield.

    pytestfixturestest-setuppython

  4. 4.A team wants to run its Postman collection of API tests automatically on every pull request, inside a CI pipeline that has no GUI. What is the standard way to do this?

    Easy

    Show answer and explanation

    Newman is Postman's official Node.js-based command-line runner: it takes an exported collection (and optionally an environment file) and executes the same requests and test scripts the collection defines, producing pass/fail output and exit codes a CI system can act on, with no browser or desktop app involved. The desktop app has no --no-gui flag, rewriting the tests elsewhere throws away working test logic for no reason, and Postman Cloud monitors are not how collections get wired into a CI pipeline job.

    postmannewmanci-cdapi-testing

  5. 5.RestAssured tests are commonly written with the given().when().then() syntax, e.g. given().header(...).body(payload).when().post("/users").then().statusCode(201).body("id", notNullValue()). What is the main reason this fluent, Gherkin-style chain is used instead of building the request and assertions as separate, ordinary Java statements?

    Medium

    Show answer and explanation

    given()/when()/then() is RestAssured's own fluent DSL, independent of any BDD framework: given() sets up preconditions, when() names the action, then() states the expected outcome — purely a readability/structure choice, not a performance optimization or JVM requirement, and it works standalone in plain JUnit/TestNG without Cucumber.

    restassuredjavaapi-testingbdd

  6. 6.In a JMeter Thread Group, you set Number of Threads (users) to 100 and Ramp-Up Period to 50 seconds, with the default loop count. What does JMeter actually do with these two numbers?

    Medium

    Show answer and explanation

    Ramp-Up Period spaces out thread (user) creation evenly across the window — 100 threads over 50 seconds means roughly one new thread every 0.5 seconds — so load builds up realistically instead of hitting the target as an instant spike. It does not directly set requests-per-second (that depends on per-thread request timing too), and it is not a pre-test delay; launches begin immediately and are spread across the window.

    jmeterperformance-testingload-testing

  7. 7.The classic testing pyramid is drawn with a wide base of unit tests, a narrower band of integration tests, and a small tip of end-to-end (E2E) tests. Why that shape, rather than an even split across the three?

    Easy

    Show answer and explanation

    The pyramid is a cost-and-speed argument, not a rule about validity or sequencing. Unit tests isolate logic and run in milliseconds, pointing precisely at what broke; integration tests wire together real collaborators and catch wiring/contract bugs unit tests can't see; E2E tests give the highest confidence but are slowest and priciest to maintain. No framework mandates unit tests, E2E tests aren't 'invalid,' and there's no rule about write order.

    testing-pyramidunit-testingintegration-testinge2e-testingtest-strategy

  8. 8.A function sendWelcomeEmail(user) calls an EmailClient dependency. In a unit test, EmailClient is replaced with a test double that returns a hardcoded 'queued' response so sendWelcomeEmail can keep running, and the test never checks whether or how the double was called — it only asserts on sendWelcomeEmail's own return value afterward. Which kind of test double is being used here?

    Medium

    Show answer and explanation

    A stub returns canned answers purely to let the code under test run, and the test checks the outcome of the code under test, never inspecting the stub itself — exactly this scenario. A mock is used for behavior verification (asserting specific calls/args happened), which doesn't occur here. A fake is a genuinely working lightweight implementation, not one hardcoded value. A spy requires inspecting recorded calls, which this test never does.

    test-doublesmockingstubbingunit-testing

  9. 9.A test, 'processes queue in order', passes every time it's run alone or as part of its own file, but fails intermittently — roughly 1 run in 15 — when the full suite runs with multiple tests executing in parallel across worker processes. What's the most likely cause to investigate first?

    Medium

    Show answer and explanation

    The key clue is that the test only fails under parallel execution, never alone — that points at cross-test contamination via shared state. A genuinely broken algorithm would fail the same way regardless of what else is running. toEqual is deterministic, the clock-drift explanation is implausible, and properly isolated tests should give the same result whether run alone or in parallel — a discrepancy is exactly the problem worth diagnosing, not something to accept as normal.

    flaky-teststest-isolationci-cdtest-reliability

  10. 10.A module has 100% line coverage from its unit tests — every line executes at least once across the test run. What does that actually guarantee?

    Easy

    Show answer and explanation

    Line coverage tracks whether a line ran, not whether the test asserted anything meaningful — a test can execute a line and assert nothing, or assert a value that happens to coincidentally match. It doesn't imply branch coverage either (only one branch of an if may ever fire), and says nothing about untested inputs, boundary values, or cross-component interactions, which unit tests (with mocked dependencies) structurally can't catch anyway.

    test-coveragetesting-metricsunit-testing

  11. 11.How would you approach this decision, and which tool would you lean toward given these requirements?

    Hard

    A team is starting a new web project: a React single-page app calling a REST API, with a login flow that redirects through a third-party SSO provider on a different domain before returning to the app. They also want to run the suite across Chromium, Firefox, and WebKit in CI, and eventually add basic mobile-web coverage. They're deciding between Cypress and Playwright for end-to-end testing.

    Show model answer

    I'd start from the concrete requirements in the prompt rather than a general preference, since both tools are strong, mature choices and the right one depends on fit. The SSO redirect is the biggest signal here. Cypress runs inside a single browser tab bound to the app's own origin by default; it does have cy.origin() specifically to support navigating to a different domain and back within one test, but using it means explicitly managing session state across that origin boundary, and it's still a narrower, more deliberate path than what Playwright offers, since Playwright drives the browser from outside the page over a separate control protocol and has no origin-bound execution context to begin with — following a real redirect to a third-party domain and back is just ordinary navigation to it. For a login flow that's core to the app, I'd rather test the real thing with as little special-casing as possible, so this pushes toward Playwright. The cross-browser requirement reinforces that: Playwright has first-class support for Chromium, Firefox, and WebKit out of the box, and WebKit support in particular is the closest thing to testing real Safari behavior without a Mac in CI. Cypress added Firefox and WebKit support later, and in my experience it's had more rough edges than its Chromium support, which was its original and still primary target. The eventual mobile-web coverage also favors Playwright, since device emulation (viewport, user agent, touch input) is a built-in, well-documented feature there. Where I'd still weigh Cypress seriously: if the team is new to end-to-end testing, Cypress's interactive Test Runner, with time-travel DOM snapshots after every command, is genuinely excellent for debugging a failing test visually and building intuition about what the app actually did at each step. Playwright's tooling has closed most of that gap — trace viewer, UI mode, and codegen cover similar ground — but Cypress's runner is still often the more approachable day-to-day authoring experience for a team just starting out. Given the specific requirements here — a real cross-domain SSO redirect, three-engine cross-browser coverage, and future mobile-web needs — I'd recommend Playwright, and would only reconsider if the team already had strong Cypress expertise and was willing to accept the extra session-handling work cy.origin() requires, which is a real but manageable cost rather than a blocker.

    A strong answer touches

    • grounds the decision in the project's actual requirements rather than a blanket tool preference
    • identifies the cross-origin SSO redirect as the key differentiator, tied to Cypress's origin-bound execution model vs Playwright driving the browser from outside the page
    • correctly notes Cypress has cy.origin() for cross-domain navigation but frames it as more deliberate/manual than Playwright's default behavior, rather than claiming Cypress simply cannot do it
    • connects the three-engine cross-browser requirement (Chromium/Firefox/WebKit) to Playwright's more mature multi-browser support
    • mentions mobile-web/device emulation as a further point favoring Playwright
    • acknowledges a genuine strength of Cypress (its interactive, time-travel test runner) rather than presenting the choice as one-sided, and reaches a clear recommendation with a stated condition for reconsidering it

    This is one genuine answer, not the only correct one — compare it with your own reasoning rather than matching it word for word.

    cypressplaywrighttool-selectione2e-testing

  12. 12.In the context of unit testing — say with Jest, or Mocha paired with Chai and Sinon — explain the difference between a mock, a stub, and a spy. When would you reach for each?

    Medium
    Show model answer

    All three are test doubles — stand-ins for a real dependency that let a unit test run in isolation — but they differ in how much of the real thing they replace and what they're used to verify. A spy wraps an existing, real function without changing its behavior: the original implementation still runs, but the spy records how it was called — how many times, with what arguments, what it returned. You reach for a spy when you want to assert that a function was called correctly, or not called at all, while still letting it do its real work. In Jest this is jest.spyOn(object, 'method'); in Mocha/Chai/Sinon it's sinon.spy(object, 'method'). A typical use: verifying that submitting a form also calls an analytics.track() function with the right event name, without needing analytics.track() to behave any differently than it normally would. A stub replaces a function's real implementation with a canned one so the test can control what it returns, without necessarily caring how it was called. You reach for a stub to force a dependency into a specific state the test needs — making a database call return a fixed list of users, or making a network call reject, so you can exercise the error-handling path without touching a real database or network. In Jest this is usually jest.fn(() => fakeValue) or jest.spyOn(...).mockReturnValue(...); in Sinon it's sinon.stub().returns(...) or .resolves(...). A mock goes a step further: it's a stub that also carries a built-in expectation about how it should be called, and the test explicitly verifies that expectation, typically failing if the calls didn't happen as specified. In Sinon, sinon.mock() lets you set expectations up front — mock.expects('method').once().withArgs(...) — and call mock.verify() to check them at the end. Jest blurs mocks and stubs together in its naming — jest.fn() and jest.mock() are commonly called 'mocks' even when they're really just controlling return values, i.e. functioning as stubs — and that looseness is common across the JS ecosystem; the distinction worth giving in an interview is behavior verification (mock) versus controlling state (stub) versus passively observing a real call (spy). In practice I default to the least invasive option that answers the question the test is actually asking: a spy when I just need to confirm a real function was called correctly, a stub when I need to control what a dependency returns to drive a specific code path, and I reserve strict mock-style call verification for the rare cases where the interaction itself, not just the resulting state, is what's actually being tested — over-using strict verification tends to couple tests to implementation details and makes them brittle against harmless refactors.

    A strong answer touches

    • defines all three as test doubles used to isolate a unit under test
    • spy: wraps a real function, keeps its behavior, records calls — used to verify a real function was called correctly
    • stub: replaces the implementation to control return values or force error paths, without necessarily asserting on how it was called
    • mock: a stub combined with a built-in expectation about calls, verified explicitly (e.g. Sinon's mock.expects()/mock.verify())
    • names concrete APIs (jest.spyOn/jest.fn, sinon.spy/stub/mock) rather than staying purely abstract
    • gives practical guidance on which to reach for and why, including the risk of brittle tests from over-using strict call verification

    This is one genuine answer, not the only correct one — compare it with your own reasoning rather than matching it word for word.

    jestmochachaisinontest-doublesmocking

  13. 13.What typically causes flaky tests in an automated UI test suite — for example, one built with Selenium or Cypress — and how would you go about fixing flakiness without just wrapping everything in retries?

    Hard
    Show model answer

    Flakiness in UI test suites almost always comes down to a handful of recurring causes, and diagnosing which one is actually at play matters more than reaching for a blanket fix like retries. The most common cause is a race between the test and the application: the test tries to interact with an element before the app has finished rendering it, loading data into it, or making it interactive — a button that exists in the DOM but is still disabled while an API call resolves, for example. The classic wrong fix is a hard-coded sleep, which is both slow, because it waits the full duration every single run, and still unreliable, because it can be too short under load or in CI where timing differs from a developer's machine. The right fix is an explicit or automatic wait tied to a real condition — Selenium's WebDriverWait combined with an expected condition like element_to_be_clickable, or Cypress's built-in automatic retrying of commands and assertions until they pass or time out — so the test waits exactly as long as needed and no longer. A second common cause is shared or leftover state between tests: a test that creates a user, leaves data in the database, or depends on running in a specific order relative to other tests will pass in isolation but fail, or pass for the wrong reason, depending on what ran before it. This gets worse as a suite grows and starts running in parallel. The fix is test isolation: each test sets up its own data, ideally through direct API calls or fixtures rather than driving the UI to seed state, which is slow and adds another layer that can itself break, and either cleans up after itself or runs against an isolated database or schema per run so tests don't depend on execution order. A third cause is brittle selectors: tests that locate elements by CSS classes or DOM structure that change for unrelated styling reasons, or by visible text that gets edited or localized. The fix is stable, purpose-built test selectors, such as a data-testid or data-cy attribute, that only change when the element's actual identity changes, decoupling tests from styling and copy churn. Environment differences are a fourth cause worth naming: a suite that's reliable locally but flaky in CI is usually exposing real race conditions that a fast, lightly loaded dev machine just doesn't trigger. Retries do have a legitimate, narrow place, as a safety net for genuinely rare, non-reproducible flakiness — TestNG supports this via an IRetryAnalyzer, JUnit through extensions or rules, and Cypress and Playwright both have built-in test-level retry options. But I'd treat retries as mitigation for the small residual flakiness that's too expensive to chase down individually, not a substitute for fixing the race conditions, shared state, and brittle selectors that cause most of it. A suite that leans on retries to pass consistently keeps eroding the team's trust in a red build, which defeats the point of having the suite at all.

    A strong answer touches

    • identifies race conditions between test actions and app state as the most common cause, and explains why hard-coded sleeps are the wrong fix
    • names the correct fix: explicit or automatic waits tied to a real condition (Selenium's WebDriverWait, Cypress's built-in retrying)
    • identifies shared or leftover state and test-order dependence as a second cause, worse under parallel execution
    • proposes test isolation via API- or fixture-based setup rather than depending on UI-driven state or run order
    • identifies brittle selectors (CSS classes, DOM structure, localized text) as a cause, and proposes stable test-specific selectors like data-testid
    • treats retries (JUnit/TestNG/Cypress/Playwright all support some form) as a narrow safety net rather than a substitute for fixing root causes

    This is one genuine answer, not the only correct one — compare it with your own reasoning rather than matching it word for word.

    seleniumcypressjunittestngflaky-teststest-reliability

  14. 14.Postman, Insomnia, and RestAssured can all be used to test a REST API, but they fit different points in a team's workflow. Compare them and explain when you'd reach for each.

    Medium
    Show model answer

    Postman and Insomnia are both GUI-based REST clients built for exploratory, manual, and collaborative API work: you construct a request by filling in method, URL, headers, and body in a window, send it, and inspect the response interactively. Postman is the heavier of the two — it adds collections with folder-level organization, environments and variables for switching between dev/staging/prod, pre-request and test scripts in JavaScript for assertions and chaining requests, mock servers, and a cloud-synced workspace for team sharing, plus Newman as a CLI runner so the same collection can execute in CI. Insomnia covers the same core loop with a lighter, often preferred-for-speed interface and strong GraphQL support alongside REST; it leans toward being the simpler, faster tool for an individual developer or small team poking at an API, though it has grown collaboration/CI features too. Both are fundamentally interactive tools: a human clicks Send and reads a response, even with scripted assertions layered on. RestAssured is different in kind: it is a Java library for writing API tests as actual code inside JUnit or TestNG, living in the project's source repository, running on every build like unit tests, and integrating directly with the codebase's models and CI pipeline without exporting anything. In practice these fit different stages: Postman or Insomnia is what a developer reaches for while building or debugging an endpoint, or what a QA engineer uses to explore a new API before formalizing tests. RestAssured (or an equivalent code-based tool) is what encodes the resulting test cases into the durable, CI-enforced regression suite. A mature team typically uses both: manual/collaborative tooling for exploration and one-off debugging, and code-based tests for the automated suite. Newman narrows the gap by letting a Postman collection run unattended in CI, but it still executes the same request/response assertions built by hand in the GUI, rather than being written and reviewed as source code the way a RestAssured test is.

    A strong answer touches

    • Postman and Insomnia are GUI REST clients for exploratory/manual and collaborative testing, with collections, environments, and scripting
    • Postman is more feature-heavy (mock servers, team workspaces, Newman for CI); Insomnia is generally lighter/faster with strong GraphQL support
    • RestAssured is a Java code library: tests live in the repo, run like unit tests, integrate with the existing build and CI without exporting anything
    • typical split: GUI tools for exploration/debugging during development, code-based tests (RestAssured) for the durable automated regression suite
    • Newman narrows the gap by running Postman collections unattended in CI, but the tests are still built in the GUI, not written and reviewed as source code

    This is one genuine answer, not the only correct one — compare it with your own reasoning rather than matching it word for word.

    postmaninsomniarestassuredapi-testingtooling

  15. 15.JMeter and k6 are both used for performance and load testing, but they're built very differently. Compare them: how are tests authored, how do they scale, and when would you pick one over the other?

    Medium
    Show model answer

    JMeter is a mature, GUI-first Java application. Tests are typically built by configuring a tree of elements — Thread Groups for virtual users and ramp-up, Samplers for the actual requests (HTTP, JDBC, JMS, and many other protocols), Listeners for viewing results, Assertions and Timers for validation and pacing — saved as an XML test plan (.jmx) that can also run headlessly from the command line for real load generation. Its strengths are protocol breadth, a large plugin ecosystem, a long track record, and a GUI that lets less code-comfortable testers build a plan visually. Its weaknesses are that XML test plans are verbose and hard to meaningfully code-review or diff, the JVM's per-thread model is relatively heavy on memory so high concurrency from one machine takes tuning or a distributed setup, and the visual builder gets unwieldy for complex, branching scenarios. k6 is a newer, developer-first, code-only tool: scripts are written in JavaScript (run on a Go runtime, not Node), so a load test is a plain, versionable, diffable, reviewable file defining virtual users, stages (ramp-up/steady/ramp-down), thresholds (pass/fail criteria like 'p95 latency under 300ms' or 'error rate under 1%'), and checks (per-response assertions), run from the command line. Its goroutine-based execution model is markedly more efficient at generating high concurrency from a single machine than JMeter's thread-per-user model, and it fits naturally into CI as just another command that can fail the build on a threshold breach. Its weaknesses are a narrower protocol focus (HTTP/1.1, HTTP/2, WebSockets, gRPC, versus JMeter's much broader protocol list) and no built-in GUI builder. In practice: pick JMeter when the target isn't a plain web API (a queue, a database directly, a legacy protocol) or the team wants a visual builder and already has JMeter expertise; pick k6 when the target is HTTP/gRPC/WebSocket-based and the team wants load tests living in version control next to the app code, running as a CI gate with explicit thresholds, and efficient at high virtual-user counts on one machine. Both support distributing load across multiple machines when one isn't enough.

    A strong answer touches

    • JMeter: GUI-built, XML (.jmx) test plans, broad protocol support (HTTP, JDBC, JMS, and more), JVM thread-per-user model
    • k6: JavaScript test scripts run on a Go runtime, code-first and version-control friendly, goroutine-based virtual users
    • k6 has built-in thresholds (pass/fail criteria) that fit naturally into a CI gate; JMeter needs extra plugins/scripting for the same
    • JMeter scales high concurrency less efficiently per machine than k6 due to its thread-per-VU model; both support distributed/multi-machine load generation
    • choose JMeter for protocol breadth or an existing GUI-based workflow; choose k6 for HTTP/gRPC/WebSocket tests that should live as code in CI

    This is one genuine answer, not the only correct one — compare it with your own reasoning rather than matching it word for word.

    jmeterk6performance-testingload-testingci-cd

  16. 16.You're asked to design a load test plan for an e-commerce checkout API before a big sale event. Walk through how you'd approach it: what you'd define up front, what scenarios you'd script, and how you'd decide pass/fail. (Assume the checkout flow is a sequence of REST calls: add-to-cart, apply-coupon, create-order, and charge-payment, backed by a database and a third-party payment gateway.)

    Hard
    Show model answer

    I'd start before writing a single script by pinning down what 'passing' means, since a load test without agreed success criteria just produces a graph nobody can act on. That means getting a target load figure grounded in something real — expected peak concurrent users or RPS during the sale, ideally from analytics for a comparable past event or a stated business projection — plus explicit performance budgets: acceptable response-time percentiles (e.g. p95 checkout latency under an agreed threshold, since averages hide the slow tail users actually feel), an acceptable error rate (e.g. under 1%), and hard constraints like the payment gateway's own rate limits, which the test must respect rather than accidentally hammer. Next, the environment: load testing production directly is usually unacceptable, so I'd want staging as close to production scale (hardware, DB size, caching) as feasible, flagging that testing an undersized staging environment tells you about staging, not production. For the payment gateway I'd use its sandbox/test mode, checking separately whether the sandbox has its own rate limits that would bottleneck the test artificially. I'd script the scenario as a realistic user journey, not isolated endpoint hits: a virtual user adds an item to cart, optionally applies a coupon, creates the order, then charges payment, with each step's response feeding data (cart id, order id) into the next, plus think-time between steps so the request rate isn't unrealistically dense. I'd run multiple test types: a load test at expected peak concurrency to confirm normal behavior holds; a stress test pushing well beyond expected peak to find the actual ceiling and how the system fails (gracefully with 5xxs and queuing, or catastrophically); a soak test at expected load for hours to catch slow leaks (memory growth, connection pool exhaustion, disk filling with logs); and a spike test with a sudden jump, since flash-sale traffic rarely ramps smoothly. I'd parametrize test data properly (many distinct carts, coupons, and accounts rather than one hardcoded id everyone hits, avoiding artificial lock contention) and consider whether sandbox charges need cleanup between runs. While running, I'd watch both client-side metrics (latency percentiles, error rate, throughput) and server-side metrics (CPU, memory, DB connection pool usage, queue depths) together, since the load tool shows the symptom and server dashboards show the cause. Pass/fail is exactly the budgets set at the start: did p95/p99 latency and error rate stay within threshold at target load, and separately, what load level did the stress test find as the actual breaking point, so the team knows its safety margin above expected sale traffic rather than getting just a binary result at one number.

    A strong answer touches

    • define success criteria up front: target concurrency/RPS grounded in real data, latency percentile budgets, acceptable error rate
    • use a production-like staging environment and a payment-gateway sandbox, not production or the real gateway
    • script a realistic multi-step user journey (add-to-cart -> coupon -> create-order -> charge) with data chained between steps and think-time, not isolated endpoint hits
    • run distinct test types: load (expected peak), stress (find the breaking point), soak (long-duration for leaks), spike (sudden burst)
    • vary test data to avoid artificial contention, and monitor server-side metrics alongside client-side results to diagnose failures, not just observe them

    This is one genuine answer, not the only correct one — compare it with your own reasoning rather than matching it word for word.

    load-testingperformance-testingjmeterk6test-planning

  17. 17.Compare unit, integration, and end-to-end (E2E) tests: what does each one actually verify, what's a concrete example of each for a typical web application's checkout flow, and what kind of bug would each type catch that the others would miss?

    Medium
    Show model answer

    A unit test exercises the smallest testable piece of code in isolation, dependencies stubbed/mocked away — e.g. calling calculateOrderTotal(items, coupon) directly and asserting the total for a cart with a 10%-off coupon, no real database or UI involved. It runs in milliseconds and, on failure, points almost exactly at the broken function, so it's ideal for pure logic bugs (an off-by-one, a coupon applied twice, a wrong rounding rule). What it can't catch is anything about how pieces fit together, since the boundaries are faked away. An integration test verifies two or more real components working together — e.g. calling the real OrderService against a real test database and asserting an order actually inserts the right row, updates inventory, and calls the payment client with the correct shape. This catches wiring/contract bugs unit tests miss precisely because unit tests mock the boundary that's now broken (a renamed schema field, a type mismatch, a serialization bug). It still doesn't cover the real user-facing path through the browser or cross-cutting concerns like session cookies or CSS breaking a button. An E2E test drives the actual deployed system like a real user, through a browser: log in, add to cart, apply coupon, pay, confirm the success page renders. This is the only layer that catches bugs which only exist when everything runs together — a renamed CSS class breaking a button, a JS bundle erroring only in the real browser, a staging misconfiguration, a race condition under real network latency. It's also the slowest, most expensive, and hardest to debug when it fails. None of the three layers subsumes the others — each is structurally blind to a different class of bug, which is the argument for having all three, weighted toward the cheaper, more precise layers.

    A strong answer touches

    • unit tests isolate one function/component with dependencies mocked out, run fast, and pinpoint pure logic bugs precisely
    • integration tests exercise two or more real components together (e.g. a real test database) and catch wiring/contract bugs that mocking hides from unit tests
    • E2E tests drive the full deployed system like a real user (browser, real services) and catch UI, config, and environment bugs invisible to the lower layers
    • each layer is structurally blind to certain bug classes; none of the three substitutes for the others
    • trade-off going up the pyramid: confidence in the real system increases, but speed and precision of failure localization decreases

    This is one genuine answer, not the only correct one — compare it with your own reasoning rather than matching it word for word.

    testing-pyramidunit-testingintegration-testinge2e-testingtest-strategy

  18. 18.Explain the difference between a mock, a stub, and a fake as types of test doubles. Give a concrete example of when you'd reach for each.

    Medium
    Show model answer

    'Test double' is the umbrella term for any object substituted for a real dependency in a test; mock, stub, and fake are specific kinds of double under it that differ in what the test actually checks. A stub supplies pre-programmed canned answers with no real logic, and the test never inspects how the stub was called — only the resulting state/output of the code under test (state verification). Example: stubbing getCurrentUser() to always return a fixed admin user so a canEditPost test can assert the boolean result, without caring how many times getCurrentUser was called. A mock also supplies canned behavior, but its purpose is interaction verification: the test explicitly asserts specific calls with specific arguments happened. Example: asserting that submitting a form calls analytics.track('form_submitted', {...}) exactly once with the right payload — the test cares that the call happened correctly, which only a mock's call-assertion captures. A fake is different in kind: a lightweight but genuinely working implementation, simpler than the real thing but with real logic. Example: an in-memory UserRepository backed by a plain map that actually implements save/find/delete, letting tests exercise realistic create-then-read sequences without a real database. In practice: reach for a stub to control a dependency's return value without caring about calls; reach for a mock when the interaction itself (a side effect like an email send or an API call) is what's under test; reach for a fake when a realistic, stateful, lightweight substitute makes tests fast and meaningfully close to real behavior. Many mocking libraries blur the line technically, but the distinction is about testing intent, not which library method was used.

    A strong answer touches

    • test double is the umbrella term; stub, mock, and fake are specific kinds of double under it
    • stub: canned responses, used for state verification — the test checks the output of the code under test, not how the double was called
    • mock: canned responses plus explicit assertions that specific calls with specific arguments happened — behavior verification
    • fake: a real, working, simplified implementation (e.g. an in-memory repository), not just a hardcoded return value
    • choose based on intent: stub to control a scenario, mock to verify a side-effecting interaction, fake for a realistic lightweight substitute

    This is one genuine answer, not the only correct one — compare it with your own reasoning rather than matching it word for word.

    test-doublesmockingstubbingunit-testing

  19. 19.What makes a test 'flaky,' and what are the most common root causes? Walk through concrete strategies for actually fixing a flaky test rather than just re-running it until it passes.

    Medium
    Show model answer

    A flaky test produces different results — pass on one run, fail on another — against the exact same code, with nothing relevant changed; its outcome is nondeterministic. This matters beyond the individual test because it destroys the suite's signal: once a red build might 'just be that test being flaky again,' people re-run CI instead of investigating, real regressions slip through disguised as flakiness, and the suite's credibility erodes. Common root causes: reliance on real time (timestamp comparisons, timeouts too short under CI load, day/month-boundary logic); shared mutable state across tests (a global variable, an in-memory store, a shared file, or a database row) that another test running concurrently in a different worker also reads or writes, so the outcome depends on the timing or order of unrelated tests rather than being properly isolated; unawaited async work/race conditions (asserting before a promise or animation finishes); external dependencies (real network calls, third-party APIs, DNS); order dependence (a test only passing because an earlier test left specific state); and unseeded randomness (which can also be legitimately finding a real bug, not just noise). Fixing starts with reproducing deliberately — running the test in a tight loop under the actual triggering conditions (parallel workers, CI load), since several causes only surface under concurrency. Then match the fix to the cause: isolate state via per-test database transactions/rollbacks, in-memory instances, or fresh fixtures instead of shared globals; replace real time/network/randomness with controllable fakes (an injectable clock, mocked network, seeded RNG); and wait on the actual condition rather than a fixed sleep, using proper await or a poll-until-true helper with its own timeout. Once fixed, confirm by running the test many times under the previously-triggering conditions. Quarantining a known-flaky test (skip/tag it while investigating) is reasonable short-term triage, not a fix — indefinite quarantine throws away coverage, and blind retry-until-green scripts can mask a genuine production race condition.

    A strong answer touches

    • a flaky test passes/fails nondeterministically on unchanged code, which erodes trust in the whole suite once people start ignoring red builds
    • common causes: reliance on real time/fixed timeouts, shared mutable state across tests, unawaited async work/race conditions, external network dependencies, test-order dependence, unseeded randomness
    • fix by isolating state per test, replacing time/network/randomness with controllable fakes, and waiting on real conditions instead of fixed sleeps
    • reproduce under the actual failing conditions (e.g. parallel execution) before attempting a fix, then re-run many times to confirm it's actually fixed
    • quarantining is acceptable short-term triage, not a substitute for diagnosing the cause; blind retries can mask a genuine production race condition

    This is one genuine answer, not the only correct one — compare it with your own reasoning rather than matching it word for word.

    flaky-teststest-reliabilityci-cdtest-isolation

  20. 20.You're setting up automated testing in a CI/CD pipeline for a web application that has unit, integration, and E2E test suites. How would you structure the pipeline's stages, and what trade-offs would you make between fast feedback and thorough coverage?

    Hard

    Assume the app has a reasonably large unit test suite, a smaller integration suite that needs a real test database, and a handful of E2E scenarios that need a deployed or staging-like environment.

    Show model answer

    The guiding principle: give the fastest feedback on the most likely, cheapest-to-diagnose problems first, only spending time on slower checks once cheap ones pass — failing fast avoids burning CI minutes on a build a fast unit test would already have caught. Structure as sequential, gated stages, not one flat job. Stage 1: lint, type-check, and unit tests, on every push/PR, parallelized across workers, finishing in a couple of minutes; block the PR immediately on failure since these are cheap and precise. Stage 2: integration tests against a real/containerized test database, running only after stage 1 passes (no point provisioning infra for already-broken code); a few minutes. Stage 3: E2E tests against a staging-like deployment, covering only the highest-value user journeys deliberately, not everything, since E2E is the slowest, most brittle layer; run on every PR if fast enough, otherwise gate to merges into main plus a nightly run, accepting a small risk window for a faster day-to-day loop. Further stages (performance/load, security scans) run as non-blocking or scheduled jobs rather than gating every commit. Key trade-offs to make explicit: parallelizing/sharding shortens wall-clock time but requires proper test isolation first, since parallel runs surface shared-state flakiness a serial run would hide. Flaky E2E tests are disproportionately costly in a gating pipeline since one can block every PR; quarantine with clear ownership and a deadline, or add retries with explicit reporting, so flakiness stays visible rather than silently absorbed. Test results must actually gate merges (PR cannot merge on red), not be merely informational, or the pipeline becomes something people learn to ignore. The overall shape mirrors the testing pyramid: cheap, frequent checks gate every change; expensive, high-fidelity checks run less often but verify what only the full system running together can verify.

    A strong answer touches

    • structure the pipeline as sequential, gated stages ordered cheapest/fastest first (lint/type-check, then unit tests) so a broken build fails fast before expensive stages run
    • integration tests run after unit tests pass, against real-ish dependencies (e.g. a containerized test database); E2E tests run against a staging-like deployment and should cover only the highest-value flows
    • trade-off: running full E2E on every PR maximizes pre-merge confidence but slows feedback and costs compute, so many teams gate E2E to merge-to-main or scheduled runs instead
    • parallelizing/sharding speeds up the suite but requires proper test isolation first, or it surfaces flaky shared-state failures instead of saving time
    • test results must actually gate merges (not just be informational), and flaky tests need visible tracking — quarantine with ownership or transparent retries — rather than silent skips or indefinite blind retries

    This is one genuine answer, not the only correct one — compare it with your own reasoning rather than matching it word for word.

    ci-cdtest-strategytesting-pyramidflaky-tests

Put this into practice

Connect the idea to a worked lesson, a practical task or your interview preparation.

← All interview prep categories