Skip to content

Top 30 Unit and Integration Testing Interview Questions and Answers [2026]

September 30, 2026 10 min read
Unit Testing interview guide by Second Talent
TL;DR: Testing interviews in 2026 check whether a candidate can pick the right level of test, use test doubles without coupling tests to implementation, and keep a suite fast and reliable. The tools have moved on: JUnit 6, pytest 9, Jest 30 and Vitest 4 all shipped major releases between June and November 2025.

JUnit 6.0, released on September 30, 2025, raised the baseline to Java 17 and gave the Platform, Jupiter and Vintage modules a single version number. It also removed the old JUnit 4 based runner.

The ideas an interviewer probes have not changed: what to test at each level, when to mock, and how to stop tests from flaking. The answers below use JUnit, pytest, Jest and Vitest for examples.

Key takeaways
  1. 1Current versions as of September 2026: JUnit 6.1.3, pytest 9.1.1, Jest 30.5, Vitest 5.0 and Mockito 5.24.
  2. 2pytest 9 merged subtests into the core and added native TOML configuration under [tool.pytest].
  3. 3Testcontainers for Java 2.0 removed JUnit 4 support and renamed every module with a testcontainers- prefix.
  4. 4Vitest 4 took Browser Mode out of experimental, so component tests can run in a real browser instead of jsdom.

Testing Fundamentals

1. What is the difference between a unit, an integration and an end-to-end test?

The difference is how much of the real system runs. A unit test runs one small piece of logic with its slow or external dependencies replaced.

An integration test runs two or more real parts together, such as a repository against a real database. An end-to-end test drives the whole deployed system the way a user or client would.

The trade-off runs in one direction: each level up gives more confidence that the system works, and costs more time, more setup and more flakiness. A good answer defines the levels by what is real, not by the tool that runs them.

2. What does the test pyramid say, and what does the testing trophy change?

The pyramid says to write many fast unit tests, fewer integration tests and very few end-to-end tests, and to push each check down to the lowest level that can catch the bug. Martin Fowler's site has a detailed practical test pyramid.

Kent C. Dodds' testing trophy puts static analysis at the base and makes integration tests the largest layer, arguing they give the best confidence for their cost in front-end code.

Both models agree on the point that matters: few end-to-end tests, and a reason for every test at every level.

Pyramid of test levels from top to bottom: a few end-to-end tests of the whole deployed system, integration tests with a real database and in-process HTTP, many unit tests that run in milliseconds, and static analysis at the base.

Where the boundary between unit and integration sits varies by team. What matters is that the team agrees on it and that each test's level matches the risk it checks.

3. What makes a good unit test?

A good unit test is fast, deterministic, independent of other tests, and fails for one clear reason. The FIRST acronym (Fast, Isolated, Repeatable, Self-validating, Timely) covers most of it.

Two more properties separate strong candidates. The test checks behaviour through the public API, so a refactor that keeps behaviour does not break it.

And its name says what broke when it fails, for example rejects_expired_token rather than test_token_2.

4. How do you structure a single test?

With three parts: Arrange the inputs and collaborators, Act by calling the code once, and Assert on the result. Given-When-Then is the same idea in business language.

def test_discount_applies_to_orders_over_100():
    # Arrange
    order = Order(items=[Item(price=120)])
    # Act
    total = checkout_total(order, code="SAVE10")
    # Assert
    assert total == 108

A test with several Act steps is usually several tests. Long Arrange sections point to a missing test data builder or fixture.

5. What makes code hard to test, and how do you fix it?

Hidden dependencies make code hard to test: the current time, random numbers, global state, and objects created with new deep inside a method. The fix is to pass them in.

class TokenService {
    private final Clock clock;

    TokenService(Clock clock) { this.clock = clock; }

    boolean isExpired(Instant expiresAt) {
        return Instant.now(clock).isAfter(expiresAt);
    }
}

@Test
void tokenIsExpiredAfterDeadline() {
    Clock fixed = Clock.fixed(Instant.parse("2026-01-01T00:00:00Z"), ZoneOffset.UTC);
    var service = new TokenService(fixed);
    assertTrue(service.isExpired(Instant.parse("2025-12-31T23:59:59Z")));
}

Production passes Clock.systemUTC(); the test passes a fixed clock and never sleeps. The same idea applies in Python (pass a clock function) and JavaScript (vi.useFakeTimers() with vi.setSystemTime()).

Test Doubles and Mocking

6. What are stubs, mocks, fakes and spies?

They are all test doubles that stand in for a real dependency, and they differ in what they do. The terms come from Gerard Meszaros and are explained in Fowler's Mocks Aren't Stubs.

  • Stub: returns canned answers so the code under test can proceed.
  • Mock: is set up with expected calls, and the test verifies those calls happened.
  • Fake: a working but simplified implementation, such as an in-memory repository.
  • Spy: records how it was called, often while passing calls through to a real object.

Mockito, unittest.mock and vi.fn() can each act as a stub, mock or spy depending on how the test uses them.

7. When does mocking make a test suite worse?

When the test verifies how the code works instead of what it produces. A test that asserts every call to every collaborator breaks on any refactor, even one that keeps behaviour identical, so the suite becomes a change detector.

Flowchart for choosing a test double: use the real object unless the dependency is slow, external or non-deterministic; use a stub when only data is needed, a mock or spy when the outgoing call is the behaviour, a fake when many tests need realistic behaviour, otherwise an integration test.

Prefer state verification (check the result or the stored data) and keep interaction checks for outgoing side effects that matter, such as "an email was sent once".

Mocking value objects or the class under test itself is almost always a smell.

8. Why "don't mock what you don't own"?

Because a mock of a third-party API encodes your guess about how that API behaves, and the guess can be wrong. The tests pass while production fails.

The fix is a thin adapter you own, such as a PaymentGateway interface with a StripeGateway implementation. Unit tests mock the adapter.

A few integration tests run the real adapter against the provider's sandbox or an HTTP stub such as WireMock or Mock Service Worker.

9. How do you test code that uses timers or the current date in JavaScript?

Replace the clock with fake timers and move time forward by hand. Both Jest and Vitest provide this.

import { vi, test, expect, afterEach } from "vitest";

afterEach(() => vi.useRealTimers());

test("debounced save fires once after 500 ms", () => {
  vi.useFakeTimers();
  const save = vi.fn();
  const debounced = debounce(save, 500);

  debounced(); debounced(); debounced();
  vi.advanceTimersByTime(499);
  expect(save).not.toHaveBeenCalled();

  vi.advanceTimersByTime(1);
  expect(save).toHaveBeenCalledTimes(1);
});

The test runs in milliseconds and cannot flake on a slow CI machine. Restoring real timers after each test stops the fake clock leaking into the next one. See the Vitest timer mocking guide.

Test Design

10. How do you test the same logic against many inputs?

With a parameterized test: one test body, a table of inputs and expected outputs. Each row is reported as its own case.

@ParameterizedTest
@CsvSource({
    "racecar, true",
    "'A man, a plan, a canal: Panama', true",
    "hello, false"
})
void detectsPalindromes(String input, boolean expected) {
    assertEquals(expected, Text.isPalindrome(input));
}

The single quotes let a CSV value contain commas. pytest uses @pytest.mark.parametrize, and Jest and Vitest use test.each.

11. What is property-based testing?

Instead of choosing example inputs, you state a property that must hold for all inputs, and the library generates hundreds of cases trying to break it. When it finds a failure, it shrinks the input to the smallest case that still fails.

import json
from hypothesis import given, strategies as st

@given(st.dictionaries(st.text(), st.integers()))
def test_json_round_trip(data):
    assert json.loads(json.dumps(data)) == data

Round trips, invariants ("sorting twice equals sorting once") and comparison with a simple reference implementation are the usual properties. Hypothesis (Python), jqwik (Java) and fast-check (JavaScript) are the common libraries.

12. How do fixtures work in pytest, and what goes wrong with them?

A fixture is a function that builds something a test needs; tests request it by naming it as a parameter. Code after yield runs as teardown, and the scope argument decides whether it is rebuilt per test, per module or once per session.

@pytest.fixture
def db_session(engine):
    connection = engine.connect()
    transaction = connection.begin()
    session = Session(bind=connection)
    yield session
    session.close()
    transaction.rollback()      # every test starts from a clean database
    connection.close()

The classic mistake is a wide-scoped fixture holding mutable state: one test changes it and the next test's result depends on run order. The pytest fixture docs cover scopes and teardown.

13. How do you test that code throws the right error?

Assert on the error type and on enough of the message to prove it is the right failure, not just any failure.

// JUnit
var ex = assertThrows(IllegalArgumentException.class, () -> new Money(-1));
assertEquals("amount must be positive", ex.getMessage());

# pytest
with pytest.raises(ValueError, match="must be positive"):
    Money(-1)

// Jest or Vitest
expect(() => new Money(-1)).toThrow("must be positive");

A try/catch with no failure after the call passes when nothing is thrown, which is the bug these helpers prevent. match in pytest is a regular expression, so escape special characters.

14. What does TDD give you, and when do you skip it?

Test-driven development means writing a failing test, making it pass with the simplest code, then refactoring: red, green, refactor. It gives a fast feedback loop and pushes the design towards small units with explicit dependencies.

It fits well-understood logic with clear inputs and outputs, such as pricing rules or parsers. It fits less well while exploring an unknown API or a UI layout, where many teams spike first and add tests once the shape settles.

Either answer is fine; the candidate should be able to say why.

Integration Testing

15. Should integration tests use an in-memory database or a real one?

A real one, in a container, for anything that touches SQL. In-memory substitutes such as H2 or SQLite differ from Postgres or MySQL in types, locking, JSON functions and constraint behaviour, so tests can pass while production queries fail.

Testcontainers starts a disposable database for the test run from Java, Python, Node and other languages. In-memory fakes still have a place at the unit level, behind a repository interface.

H2 or SQLite in memory
  • Starts instantly, no Docker needed
  • Different SQL dialect, types and locking
  • Can pass while production queries fail
Real database in a container
  • Same engine and version as production
  • Seconds to start; share one per test run
  • Needs Docker or a compatible runtime in CI

16. How do you test an HTTP API?

Mostly in-process: start the application inside the test and send requests through its router without a real network. That tests routing, validation, serialization and error handling in milliseconds.

from fastapi.testclient import TestClient
from app.main import app

client = TestClient(app)

def test_create_order_returns_201():
    response = client.post("/orders", json={"sku": "A1", "qty": 2})
    assert response.status_code == 201
    assert response.json()["qty"] == 2

Spring's MockMvc and Node's supertest do the same job. A small number of out-of-process tests against a deployed environment then check what in-process tests cannot: configuration, networking and the real server.

17. What is contract testing, and when is it better than end-to-end tests?

Contract testing checks that two services agree on their API without running both at once. In consumer-driven contract testing, the consumer's tests record the requests it makes and the responses it expects.

The provider's build then replays that contract against the real provider.

It replaces most cross-service end-to-end tests, which are slow and need every service deployed together. Pact is the common tool. It does not test business flows across services; a few end-to-end tests still cover those.

18. How do you keep integration tests isolated from each other?

Give every test its own data and clean up after it. The usual techniques are a transaction rolled back after each test, as in question 12, or unique IDs per test when rollback is impossible.

Build data with factories or builders instead of a shared seed file that every test depends on. Tests that pass alone and fail together almost always share data. Running the suite in random order, or in parallel, exposes the problem early.

Suite Quality and Speed

19. What does code coverage tell you, and what does it not?

Coverage tells you which lines or branches ran during the tests. It does not tell you whether the tests checked anything: a test with no assertions can reach full coverage.

It is most useful in reverse, to find important code that no test runs. Branch coverage is stricter than line coverage because it counts both sides of each condition.

A hard 100% target tends to produce low-value tests of getters and generated code.

20. What is mutation testing?

Mutation testing measures whether the tests catch bugs. The tool makes small changes to the code, such as turning > into >= or removing a call, and reruns the tests. A mutant that no test catches shows a gap that coverage would miss.

PIT (Java), Stryker (JavaScript, C#, Scala) and mutmut (Python) are common. It is slow, so teams run it on critical modules or on changed code only.

21. What causes flaky tests, and how do you fix them?

Most flaky tests come from a few causes: real time and sleeps, shared state between tests, test order, async code that is not awaited, and network calls.

  • Replace sleep with fake clocks or with waiting for a condition.
  • Give each test its own data and reset shared state.
  • Await every promise or future the test starts.
  • Stub external services at the HTTP boundary.

A flaky test found in CI should be fixed or quarantined within days. Retrying it automatically hides a real bug about as often as it hides a test bug.

22. How do you speed up a slow test suite?

Measure first: most suites have a few slow tests that dominate. Then run tests in parallel (pytest-xdist, JUnit's parallel execution setting, Vitest's worker pool), which requires the isolation from question 18.

Push checks down the pyramid, share expensive setup such as one database container per run, and in CI run only the tests affected by a change before the full suite. Removing sleeps is often the biggest single win.

23. How do you add tests to legacy code that has none?

Start with characterization tests: tests that record what the code does today, right or wrong, so a refactor cannot change behaviour unnoticed.

Michael Feathers' approach is to find a seam, a place to swap a dependency without editing much code, and test through it.

Test at a higher level first, often through the API, because the internals are not yet testable. Break dependencies in small, safe steps, then add unit tests as the code gets smaller units.

24. How do you test asynchronous code without false passes?

Make the test wait for the async work, and assert on its result. A test that starts a promise and returns early passes before the assertion runs.

test("loads the user", async () => {
  await expect(loadUser(1)).resolves.toMatchObject({ id: 1 });
});

test("rejects unknown users", async () => {
  await expect(loadUser(999)).rejects.toThrow("not found");
});

Forgetting await before expect(...).resolves is the common bug. In pytest, async tests need a plugin such as pytest-asyncio or AnyIO's pytest plugin.

25. Should you test private methods?

No, test them through the public methods that use them. A private method is an implementation detail, and tests that reach into it break whenever it changes.

If a private method feels like it needs its own tests, it usually holds logic that belongs in its own class with a public API. Extracting it makes the tests straightforward and the design clearer.

What Changed Recently

Jun 4, 2025
Jest 30: drops old Node versions, jsdom 26.
Sep 30, 2025
JUnit 6.0: Java 17 baseline, one version number.
Oct 14, 2025
Testcontainers for Java 2.0: JUnit 4 support removed.
Oct 22, 2025
Vitest 4: Browser Mode stable, visual regression testing.
Nov 2025
pytest 9.0: subtests, native TOML config, strict mode.

26. What changed in JUnit 6?

According to the JUnit 6.0 release notes, the headline changes are:

  • A Java 17 and Kotlin 2.2 baseline.
  • One version number for Platform, Jupiter and Vintage, instead of 1.x for the Platform and 5.x for Jupiter.
  • JSpecify annotations that mark which parameters and return values can be null.
  • Kotlin suspend functions allowed as test methods.
  • Removal of junit-platform-runner, the JUnit 4 based runner for the Platform.

For most projects on Java 17 or later, the upgrade is a version bump. Projects still on Java 8 or 11 must stay on JUnit 5.

27. What are subtests in pytest 9, and when would you use them instead of parametrize?

Subtests report several failures from one test separately, and they became part of pytest core in pytest 9.0. Use them when the cases are only known at run time, which parametrize cannot handle because it needs values at collection time.

def test_py_files_contain_docstring(subtests: pytest.Subtests) -> None:
    for path in Path.cwd().glob("*.py"):
        with subtests.test(path=str(path)):
            assert contains_docstring(path)

Each failing file is reported on its own instead of the loop stopping at the first one. The release notes call the feature experimental in how failures are reported, but stable in how it is used.

28. What breaks when a project upgrades to Jest 30?

The Jest 30 announcement lists the main breaking changes:

  • Node 14, 16, 19 and 21 are no longer supported, and TypeScript 5.4 is the minimum.
  • jest-environment-jsdom moved from jsdom 21 to 26, which breaks some tests that mock window.location.
  • Several expect aliases were removed; eslint-plugin-jest has an autofix for them.
  • --testPathPattern was renamed --testPathPatterns.

Non-enumerable properties are also no longer compared by toEqual, which can change the result of existing assertions.

29. What does Vitest Browser Mode offer over jsdom?

It runs component tests in a real browser engine, so layout, CSS, focus and real events behave as they do for users. jsdom only simulates the DOM in Node and has no layout engine.

Vitest 4 removed the experimental tag from Browser Mode and added visual regression testing. Providers now come as separate packages, such as @vitest/browser-playwright.

The trade-off is speed: jsdom is still faster for logic that does not depend on layout. Vitest 5.0 followed on September 3, 2026.

30. What changed in Testcontainers for Java 2.0?

Per the 2.0.0 release notes, JUnit 4 support was removed and every module was renamed with a testcontainers- prefix. Container classes also moved into per-module packages.

// Before 2.0
testImplementation("org.testcontainers:mysql")
import org.testcontainers.containers.MySQLContainer;

// 2.0
testImplementation("org.testcontainers:testcontainers-mysql")
import org.testcontainers.mysql.MySQLContainer;

An upgrade therefore touches build files and imports, and any JUnit 4 tests that use containers must move to JUnit Jupiter first.

Version check before the interview. Ask which JUnit, pytest or Jest version the candidate's current team runs. Someone still on JUnit 5 or Jest 29 has no reason to know the details above, but should know how they would find them.

Signs of a Strong Answer

  • They define test levels by what is real (database, network, browser), not by the tool.
  • They prefer state checks and explain when an interaction check is worth it.
  • They reach for a real database in a container over H2 or SQLite for SQL code.
  • They fix time, randomness and ordering before calling a test "flaky".
  • They treat coverage as a map of untested code, and know about mutation testing.
  • They know the major versions their stack runs and what the last upgrade broke.

Hiring Engineers Who Test

Second Talent places pre-vetted back-end engineers from Asia who write tested code, screened with questions like these.

Tell us the stack and we send a shortlist within 24 hours. Start hiring, or see our load testing and Playwright interview guides.

Hiring developers in Southeast Asia?

Get Cost Guide

How would you like to talk?

WhatsApp us Prefer texting at your own pace? Just hit us up on WhatsApp. We promise no spam and a hassle-free experience.

Loading available times…