Software Testing & QA Interview Questions and Answers

Test design, unit/integration/E2E, mocking, TDD and quality strategy.

Practise 10 random 12 peer-reviewed questions
Software Testing & QA Interview Syllabus & Preparation Strategy

Whether you are preparing for entry-level Software Testing & QA interview questions for freshers or senior software engineer interview questions addressing concurrency, scalability, and system architecture, this track provides peer-reviewed model answers with syntax walkthroughs, edge cases, and practical interview tips.

1 What is the test pyramid? Easy

The test pyramid is a guideline for balancing automated tests by level.

      /\        E2E (few, slow, brittle)
     /  \
    /----\      Integration (some)
   /------\
  /--------\    Unit (many, fast, cheap)

The base is unit tests: numerous, fast, isolated, checking small pieces. The middle is integration or service tests, verifying that components work together, including databases and HTTP boundaries. The top is end-to-end tests through the UI, which give the most confidence that the whole system works but are slow, flaky and expensive to maintain.

The shape matters because tests have different costs and failure signals. A pyramid catches most bugs cheaply, while an inverted shape, the ice cream cone of many E2E tests, leads to slow pipelines and painful maintenance.

The model is a heuristic, not a law. For a data pipeline, integration tests may dominate. Some teams prefer a testing trophy that emphasises integration tests as the best confidence-to-cost ratio.

2 What is the difference between unit, integration and end-to-end tests? Easy

Unit tests exercise a single function, class or module in isolation, with dependencies replaced by fakes. They run in milliseconds, pinpoint failures and are cheap to write. Their weakness is that they say nothing about how pieces fit together.

Integration tests verify that two or more real components work together: a repository against a real or containerised database, a service against an HTTP client, a queue producer and consumer. They catch wiring, serialisation, transaction and configuration bugs, and are slower but still fast enough for CI.

End-to-end tests drive the whole system through its real interfaces, often via a browser with a tool like Playwright, covering the user journey. They give the strongest confidence that the product works, but they are the slowest and most brittle, and failures are harder to diagnose.

unit < integration < e2e    (speed and count)
unit > integration > e2e    (confidence scope, reversed)

Use all three, with most investment near the bottom.

3 What makes a good unit test? Easy

A good unit test is fast, deterministic, isolated and focused on behaviour.

Characteristics:

  • Tests one behaviour, with a clear name such as rejects_negative_amount, so a failure tells you what broke.
  • Follows Arrange-Act-Assert, keeping setup, action and verification distinct.
  • Has no dependence on execution order, shared mutable state, the clock, the network or random values; inject these as dependencies.
  • Asserts on observable outcomes rather than internal implementation, so refactoring does not break it.
  • Has a single logical assertion concept; multiple asserts are fine if they describe one behaviour.
  • Fails with a message that shows expected versus actual.
  • Runs in milliseconds, so the suite can run on every save.
@Test void rejectsNegativeAmount() {
    assertThrows(IllegalArgumentException.class,
        () -> account.deposit(-1));
}

Avoid over-mocking, which couples tests to implementation. A test that never fails, or one that fails for unrelated reasons, costs more than it protects.

4 What is the difference between mocks and stubs? Medium

Test doubles stand in for real collaborators. The common kinds differ in what they verify.

  • Dummy: passed but never used, just to satisfy a signature.
  • Stub: returns canned answers, for example a repository returning a fixed user.
  • Spy: a stub that also records how it was called.
  • Mock: pre-programmed with expectations and verified, so it asserts interaction.
  • Fake: a working but lightweight implementation, such as an in-memory database.
when(repo.find(1)).thenReturn(new User("a")); // stub
verify(email).send(any());                    // mock, checks interaction

Stubbing is about supplying indirect inputs; mocking is about verifying indirect outputs. Prefer stubs and fakes for state-based tests, because asserting on every call couples tests to implementation and makes refactoring painful. Use mocks when the interaction itself is the contract, such as an event being published exactly once. Overusing mocks produces tests that pass while the real integration is broken.

5 Explain the TDD red-green-refactor cycle. Medium

Test-driven development is a short cycle that drives design from tests.

  1. Red: write a failing test for the smallest next behaviour. It must fail, otherwise it proves nothing.
  2. Green: write the simplest code that makes it pass, without worrying about elegance.
  3. Refactor: improve the code and tests, removing duplication and clarifying names, while keeping the suite green.
red -> green -> refactor -> repeat

Because tests come first, the code is testable by construction, interfaces are designed from the caller's perspective, and each requirement has a test. The tight feedback loop encourages small steps and prevents scope creep.

Criticisms are real: it does not suit exploratory or UI-heavy work well, it can produce fragmented designs if driven mechanically, and it demands discipline. Variants exist, such as behaviour-driven development with Given-When-Then scenarios, and test-first is also common without the strict refactor step. The lasting value is the safety net and incremental design.

6 Compare black-box and white-box testing. Medium

Black-box testing treats the system as an opaque component and derives tests from requirements and the interface, without looking at the code. Testers exercise inputs and observe outputs, focusing on behaviour. Techniques include equivalence partitioning, boundary value analysis, decision tables and state transition testing. It benefits from an independent perspective, can find missing or misunderstood requirements, and is how acceptance testing is usually done.

White-box testing uses knowledge of the implementation to design tests. Coverage of statements, branches, paths and conditions guides what to exercise. It can target tricky logic and reveal unreachable or untested code, and it is common in unit tests.

black box: inputs -> [ system ] -> outputs
white box: inspect conditionals, loops, branches

They are complementary. Black-box testing checks that the right thing was built; white-box testing checks that it is built thoroughly. Grey-box testing combines both, for example using schema knowledge to craft API tests.

7 Explain boundary value analysis and equivalence partitioning. Medium

Most bugs hide at the edges of valid ranges, because off-by-one and comparison mistakes concentrate there. Boundary value analysis tests just inside, on and just outside each boundary.

For a valid range of 1 to 100, test 0, 1, 2, 99, 100 and 101. Equivalence partitioning complements it by dividing the input domain into classes expected to behave the same, then testing one representative from each: any number below 1, any in range, any above 100.

partition: (-inf,0] [1,100] [101,inf)
boundary:     0  1 ... 100  101

Boundaries also apply to collections: empty, one element, many and maximum. And to types: zero, negative, very large, null and empty string. For date logic, test month ends, leap years, year boundaries and time zones.

Combine the two techniques: partitions decide what to test, boundaries decide exactly where. This gives strong coverage with few cases.

8 What are flaky tests and how do you fix them? Medium

A flaky test passes and fails on the same code. Flakiness is corrosive because teams start ignoring failures, which hides real regressions.

Common causes:

  • Timing and asynchronous waits: fixed sleeps, race conditions, animations.
  • Shared state: tests depending on order, a shared database or global singletons.
  • External dependencies: network, third-party APIs, real clocks and random generators.
  • Concurrency: parallel test runs colliding on ports, files or data.
  • Resource limits: timeouts that fail on slow CI machines.

Fixes:

  • Wait for conditions or events, not fixed durations.
  • Isolate state, reset the database per test or use transactions and unique data.
  • Seed randomness and freeze or inject the clock.
  • Quarantine and fix the worst offenders rather than retrying blindly.
  • Add retries only as a stopgap and log diagnostics.

Track flake rate over time and treat it as a quality metric, because a suite people trust is worth far more than a large one they ignore.

9 What are the limitations of code coverage? Medium

Code coverage measures which lines, branches or paths tests execute. It is useful for finding untested code and spotting dead code, and it prevents coverage from silently dropping.

Its limits are important. Coverage says nothing about whether assertions are meaningful. A test can execute every line and assert nothing. It does not prove all input partitions or boundary cases are covered, and high coverage can coexist with serious bugs, including security and concurrency defects. Chasing a number also encourages trivial tests that touch code without checking behaviour.

coverage: did we execute it?
tests:    did we verify it?

Use coverage as a diagnostic and a trend, not a target. A common approach is a modest gate, for example 70 to 80 percent on changed code, plus review of whether critical paths and branches are genuinely tested. Branch and mutation coverage give stronger quality signals, and some code such as generated files should be excluded.

10 Write unit tests for a pricing function. Medium

Given a pricing function, write focused tests with JUnit and AssertJ.

class PriceCalculator {
    BigDecimal finalPrice(BigDecimal base, int qty, boolean member) {
        if (qty <= 0) throw new IllegalArgumentException("qty");
        BigDecimal total = base.multiply(BigDecimal.valueOf(qty));
        return member ? total.multiply(new BigDecimal("0.9")) : total;
    }
}

class PriceCalculatorTest {
    private final PriceCalculator calc = new PriceCalculator();

    @Test void multipliesByQuantity() {
        assertThat(calc.finalPrice(new BigDecimal("10"), 3, false))
            .isEqualByComparingTo("30");
    }
    @Test void appliesMemberDiscount() {
        assertThat(calc.finalPrice(new BigDecimal("10"), 1, true))
            .isEqualByComparingTo("9");
    }
    @Test void rejectsNonPositiveQuantity() {
        assertThatThrownBy(() -> calc.finalPrice(BigDecimal.TEN, 0, false))
            .isInstanceOf(IllegalArgumentException.class);
    }
}

The tests cover the happy path, a rules branch and an invalid input, use names that state the behaviour, and avoid floating-point equality by comparing BigDecimal values.

11 How do you add tests to legacy code that has none? Hard

Legacy code is often defined as code without tests, which makes changing it risky. The core technique is to introduce a seam, a place where behaviour can be substituted without editing the code under test.

Michael Feathers' approach:

  1. Identify a change point and find the smallest behaviour to pin down.
  2. Break dependencies by extracting interfaces, wrapping static or global calls, and injecting collaborators through constructors or parameters.
  3. Write characterisation tests that capture current behaviour, even if it looks wrong, so you know when you change it.
  4. Refactor under that safety net in small steps, then add tests for the new behaviour.
class Report {
    Report(Clock clock, Mailer mailer) { ... } // seams injected
}

Tools help: approval or golden-master tests capture output for comparison, and dependency-breaking tools can substitute calls at link time. The risk is over-mocking, so prefer real fakes where feasible. Do not aim for full coverage first; cover the areas you are about to change.

12 How would you design a test strategy for a microservices system? Hard

Testing a distributed system needs a layered strategy, because full end-to-end coverage does not scale.

  • Unit tests per service for domain logic, run on every commit.
  • Component tests that start one service with its dependencies faked, to test its API and data layer.
  • Contract tests, using consumer-driven contracts such as Pact, to verify that a provider still satisfies what consumers expect. This catches integration breaks without a full environment.
  • Integration tests against real infrastructure for databases, queues and external gateways, ideally with testcontainers.
  • A small number of end-to-end journeys, run against staging, for critical flows.
  • Non-functional tests: load, resilience and chaos, to check behaviour under failure.
unit -> component -> contract -> integration -> e2e

Support this with test data management, environment parity, observability in CI and fast feedback. Prefer testing at the lowest layer that can catch a given bug, and make failures easy to localise, because debugging a red pipeline across ten services is the real cost.

Frequently Asked Questions About Software Testing & QA Interviews

What do hiring managers evaluate in Software Testing & QA technical rounds?

Technical interviewers look for foundational fluency, idiomatic syntax, clarity when communicating complex logic, and awareness of performance trade-offs (e.g. memory footprint, render performance, and network latency) in production environments.

What are the best interview tips for practicing Software Testing & QA questions?

Use active recall: summarize each answer in your own words before revealing the model solution. Focus on explaining why a certain approach is chosen rather than just memorizing code syntax.