27 questions

Testing interview questions and answers

Junior questions

  1. 1. How do unit, integration, and e2e tests differ from each other?

    Answer

    A unit test checks the smallest piece of logic, a function or a class, in isolation from the rest, and runs in milliseconds. An integration test checks the interaction of several modules, for example a service with a real database or a component with a store. An e2e test runs the whole application through the interface like a user: browser, backend, database. The higher the level, the more confidence in the system, but the slower the run and the harder the diagnosis. So they are combined rather than picked one at a time.

    Follow-up question: An example of behavior a unit test misses but an integration test catches?

  2. 2. How do a stub, mock, spy, and fake differ?

    Answer

    All of these are test doubles, replacements for real dependencies in a test. A stub returns predefined answers. A mock additionally verifies interactions: whether a method was called and with which arguments. A spy wraps a real function and records calls without replacing the behavior. A fake is a simple working implementation, for example an in-memory database. Excessive mocks tie a test to the implementation, so where possible a fake or the real dependency is better.

    Follow-up question: Why do tests with many mocks break during refactoring?

  3. 3. Does high code coverage guarantee the quality of tests?

    Answer

    No. Coverage shows which lines were executed during tests, but not whether they verified the result. A test without assertions can yield 100% coverage. The metric does not see missed scenarios, boundary values, and complex combinations. It is more useful to treat coverage as a way to find untested areas rather than as a goal. The quality of the tests themselves is measured by mutation testing and review, not by a percentage.

    Follow-up question: What is mutation testing, and which coverage problem does it reveal?

  4. 4. What is TDD, and what are its advantages and drawbacks?

    Answer

    TDD is the red, green, refactor cycle: first you write a failing test, then the minimal code that passes it, then you refactor under green tests. Advantages: testable design, fast feedback, behavior documentation, and less fear of changes. Drawbacks: a slower start, difficulty with experimental or UI-heavy work, and poorly written tests cement bad design. TDD works best for clear business logic and bug fixes, where you first write a test that reproduces the error.

    Follow-up question: How does TDD differ from writing tests after the code?

  5. 5. What makes a good test structure, and what are Arrange, Act, Assert?

    Answer

    A test is conveniently split into three parts: Arrange prepares data and dependencies, Act performs the single action under test, Assert checks the result. One test should verify one behavior, so that on failure it is clear what broke. The test name describes the scenario and expected outcome, for example "returns an error if the email is empty". Tests must be independent and not rely on order. Readable tests work as documentation.

    Follow-up question: Are multiple assertions in one test acceptable?

  6. 6. How do you test asynchronous code correctly in Jest or Vitest?

    Answer

    A test function can be async, and then the runtime waits for its promise. You use await or the resolves and rejects matchers. A classic mistake is forgetting await or return, so the test finishes before the check and is always green. For errors you verify that the promise is rejected with the expected message rather than merely catching an exception. For callbacks and timers you use fake timers or waitFor, not arbitrary pauses. It helps to set expect.assertions(n) to ensure that assertions really ran.

    Follow-up question: Why can a test without await pass even if the code is broken?

  7. 7. Why use parameterized tests?

    Answer

    A parameterized test runs one check logic over a set of inputs: test.each in Jest, it.each in Vitest, @pytest.mark.parametrize in pytest. This removes copy-paste and makes edge cases explicit: zero, an empty string, negative values, the maximum. Each data set is reported as a separate test, so you can see exactly which one failed. You should not turn the table into complex logic with conditions, because then the test is hard to read.

    Follow-up question: How do you name individual table cases so the report is clear?

  8. 8. What is a regression test, and how do you add one when fixing a bug?

    Answer

    A regression test verifies that a previously fixed error or existing behavior does not break again after changes. The best practice: before fixing a bug, write a test that reproduces it and fails, then fix the code and confirm the test turns green. This proves the test actually catches the problem, so the bug will not return unnoticed. The test is named by behavior rather than ticket number, although an issue link in a comment is useful. The regression suite grows with the product, so its speed must be kept in check.

    Follow-up question: Why shouldn't you write the test only after the fix?

Middle questions

  1. 1. How does the testing pyramid differ from the testing trophy?

    Answer

    The pyramid recommends many fast unit tests, fewer integration tests, and very few e2e tests, because upper levels are slower and more brittle. The testing trophy, popularized by Kent C. Dodds, emphasizes integration tests: they give the best ratio of confidence to cost, while static analysis (TypeScript, linter) catches some errors almost for free. For UI code, component integration tests with real dependencies are closer to user behavior than isolated units full of mocks. Both models are heuristics, and the proportions depend on the product.

    Follow-up question: How do you decide which test level to choose for a new feature?

  2. 2. How do you test code that depends on time: timers, debounce, dates?

    Answer

    Real setTimeout makes a test slow and flaky, so fake timers are used: jest.useFakeTimers() or vi.useFakeTimers(). They let you advance time manually with advanceTimersByTime and run deferred callbacks instantly. For dates you can freeze the current time with vi.setSystemTime. A better design is to pass a clock or a now function as a dependency, so the test simply supplies the needed value. After the test you must restore real timers so other tests are not affected.

    Follow-up question: How do fake timers behave with promises and async/await?

  3. 3. How do you replace network requests in tests, and which approach is best?

    Answer

    You can mock fetch or the client directly, but that ties the test to the implementation. It is better to intercept requests at the network level, for example with the MSW (Mock Service Worker) library, which returns responses by URL and method regardless of how the code makes the request. The same set of handlers works for unit tests, Storybook, and development. In Playwright e2e there is page.route, which replaces responses for specific requests. Error scenarios are tested separately: 500, timeout, empty response.

    Follow-up question: How do contract tests complement network mocks?

  4. 4. What are flaky tests, why do they occur, and how do you fight them?

    Answer

    A flaky test sometimes passes and sometimes fails on the same code. Typical causes: races and wrong waiting (fixed sleep instead of waiting for a condition), shared state between tests, dependence on time, execution order, or external services, and unstable selectors. The cure is to isolate each test's data, use auto-waiting and explicit conditions, freeze time and network, and remove order dependence. Retries in CI only mask the problem, so they are acceptable temporarily along with a flaky-test metric. An unstable test is better isolated and fixed than ignored, because the team stops trusting a red CI.

    Follow-up question: How do you find a flaky test among hundreds if it fails once in twenty runs?

  5. 5. How does Playwright differ from Cypress, and which should you pick for a new project?

    Answer

    Cypress runs inside the browser and has a convenient interactive mode with time-travel debugging, but is historically limited to one tab and domain. Playwright controls the browser from outside through a protocol, supports Chromium, Firefox, and WebKit, multiple tabs and contexts, and parallel runs out of the box. It has auto-waiting, tracing (trace viewer), and supports TypeScript, Python, Java, and .NET. For a new project that values cross-browser coverage and CI speed, Playwright is usually chosen. Cypress remains a good choice if the team values its debugging experience and already knows it.

    Follow-up question: How do you isolate state between tests in Playwright?

  6. 6. When are snapshot tests useful, and when are they harmful?

    Answer

    A snapshot test stores serialized output (a component tree, JSON) in a file and compares against it on later runs. It is useful for small stable structures: error messages, generated code, configs. For large component snapshots it is harmful: any markup change breaks dozens of tests, developers mindlessly run -u, and the test stops verifying anything. A snapshot also does not explain which behavior was expected. It is better to write targeted assertions on meaningful behavior and keep snapshots for small, well-bounded data.

    Follow-up question: What are inline snapshots, and why are they better?

  7. 7. How do you test code that works with a database?

    Answer

    Mocking the database in a test verifies almost nothing, because SQL, transactions, and constraints behave differently. It is more reliable to run tests against a real database of the same kind, for example in a container (Testcontainers) with migrations applied. For isolation, each test runs in a transaction that is rolled back, or on a separate schema or temporary database. Data is prepared with factories or fixtures rather than a shared dump. SQLite instead of Postgres gives fast tests but hides dialect differences, so for critical logic a real DBMS is better.

    Follow-up question: How do you speed up a test suite that uses a real DB?

  8. 8. Why do load testing, and what does it look like in k6?

    Answer

    Load testing checks how a system behaves under expected and peak traffic: latency, error rate, throughput, bottlenecks. In k6 the scenario is written in JavaScript, virtual users and duration are set in options, and thresholds such as p(95)<500 make a CI run pass or fail. They distinguish smoke, load, stress, spike, and soak tests. Results must be read together with server metrics, otherwise it is unclear where exactly the bottleneck is.

    Follow-up question: Why do load tests look at p95 and p99 rather than the average?

  9. 9. What is the Testing Library philosophy, and why does it recommend querying elements by role?

    Answer

    Testing Library advises testing the application the way a person uses it, rather than checking internal state or component structure. So elements are found by role, label, and visible text (getByRole, getByLabelText), and data-testid is a last resort. Such queries also verify accessibility: if a button cannot be found by role, it is hard to use with a screen reader. Interactions are better simulated through userEvent, which reproduces the real sequence of events. Tests become more resilient to refactoring because they do not depend on implementation.

    Follow-up question: How do you wait for an asynchronous UI update in Testing Library?

  10. 10. What are fixtures in pytest, and why are they better than setUp/tearDown?

    Answer

    A fixture is a function decorated with @pytest.fixture that prepares a resource, and a test requests it by name as a parameter. With yield you can define cleanup code that runs after the test. Fixtures have scopes (function, module, session), can depend on one another, and can live in conftest.py, making them available to all tests in the folder. Unlike setUp, they are explicit in the test signature, composable, and loaded only where needed. Together with @pytest.mark.parametrize they give compact tests for many inputs.

    Follow-up question: When should you raise a fixture's scope to session, and what is the risk?

  11. 11. How do you write stable selectors in e2e tests?

    Answer

    Selectors tied to CSS classes or DOM structure break on every redesign. More stable are queries by role and accessible name (getByRole('button', { name: 'Save' })), by field label, or by visible text. When that is not enough, you add dedicated data-testid attributes that do not depend on styles. Complex XPath and nth-child chains are avoided. The Page Object pattern also helps: selectors live in one place, so a markup change touches a single file.

    Follow-up question: What accessibility benefit do role-based queries bring?

  12. 12. How does BDD differ from TDD, and what is Gherkin for?

    Answer

    TDD is developer-oriented: a test is written before the code at the unit level. BDD describes system behavior in language understandable to the whole team and shifts the focus to a shared understanding of requirements between development, testing, and the business. Gherkin is the Given / When / Then format in which scenarios are written as specifications, and tools (Cucumber) bind steps to code. The benefit exists when the business actually reads and agrees on the scenarios. If only QA or developers write them, Gherkin adds a layer of abstraction without a payoff.

    Follow-up question: In which situations does Gherkin hurt more than it helps?

  13. 13. How do you organize test data: fixtures, factories, a shared dump?

    Answer

    A shared database dump makes tests dependent on one another and hard to understand. Factories (factory functions, factory_boy, fishery) create objects with sensible defaults, and a test overrides only the fields that matter for the scenario. That makes it clear what is significant. Random data (faker) helps find assumptions but needs a fixed seed so failures are reproducible. Each test creates its own data and cleans it up rather than relying on the state left by the previous test.

    Follow-up question: How is the builder pattern useful for complex domain objects in tests?

  14. 14. When are component tests better than e2e for the frontend?

    Answer

    Component tests (Testing Library in jsdom, or a real browser in Vitest Browser Mode or Playwright Component Testing) are fast, isolated, and show exactly which component broke. They are good for checking component states: loading, error, empty list, validation. E2e is needed for end-to-end paths that depend on routing, authentication, the backend, and browser behavior, such as sign-up and payment. A practical balance: many component tests for states and branches, few e2e for critical scenarios. jsdom does not implement layout and some APIs, so visual aspects are better verified in a real browser.

    Follow-up question: What are visual regression tests, and where do they fit?

  15. 15. Can accessibility checks be automated, and what are the limits?

    Answer

    Yes, tools such as axe-core (via jest-axe, @axe-core/playwright, cypress-axe) find a portion of violations: missing alt, contrast, wrong ARIA attributes, fields without labels. They are added to component or e2e tests and to CI. Automation catches only part of the problems, usually well under half. Focus order, label clarity, and screen reader usability need manual checking. So automated checks are combined with a manual pass through key scenarios.

    Follow-up question: Which violations will axe definitely not find?

Senior questions

  1. 1. What are contract tests (consumer-driven contracts), and when are they needed?

    Answer

    A contract test verifies that an API provider meets a consumer's expectations without running the whole system. In the consumer-driven approach (Pact), the consumer describes which requests it makes and which responses it expects, and the provider verifies this contract in its own CI. A change that breaks a consumer is thus caught before release rather than on an integration environment. Contracts are especially useful in microservices and for mobile clients that cannot be updated instantly. They do not replace checking the provider's business logic.

    Follow-up question: How do contract tests differ from e2e tests between services?

  2. 2. How do you speed up a test suite in CI when it has grown to tens of minutes?

    Answer

    First you measure what exactly is slow. Then you parallelize: workers in Jest, Vitest, and Playwright, sharding the suite across several CI machines (--shard=1/4). You cache dependencies and the build, and run only tests affected by changes (--changedSince, affected mode). Slow e2e tests are limited to critical paths, and the rest move to lower levels of the pyramid. You remove needless sleep calls and isolate data so parallel tests do not interfere with each other. Flaky tests get fixed, because retries make the suite even longer.

    Follow-up question: What problems appear when running tests in parallel on a shared database?

  3. 3. How does code design affect testability?

    Answer

    Code is easy to test when dependencies are passed in from outside (dependency injection) and logic is separated from side effects. Pure functions that take input and return a result are tested without mocks. A thin layer of "effects" (HTTP, database, file system) is isolated behind an interface, and business logic stays in the core. If a test requires mocking five modules and global variables, that signals excessive coupling. Hidden dependencies on time, randomness, and environment should also become explicit parameters.

    Follow-up question: What is hexagonal architecture from a testing point of view?

  4. 4. Should you run tests in production, and what safe options exist?

    Answer

    Full tests with side effects in production are dangerous, but some checks there are useful. Synthetic monitoring (smoke scenarios on Playwright or k6 on a schedule) with dedicated test accounts reports breakage before users do. Safe patterns: read-only checks, test tenants, and marked data excluded from analytics and billing. After release, canary deployments and feature flags with gradual rollout help. Load tests against production are acceptable only with agreement and safeguards, and they are usually run on a staging environment as close to prod as possible.

    Follow-up question: How do you distinguish test data from real data in analytics and billing?

Train these questions

Study the cards, answer by text or voice and get an AI grade. Needs a signed in account.

Answer out loud and get graded

Here you get the questions with model answers. In the ShortLesson app you answer yourself, by text or voice, and AI grades the answer against the rubric and asks a follow-up, like a real interviewer.

The app is coming to the stores soon.

Other topics