Testing interview questions
Testing questions a Backend developer can be asked, from first job to senior. Open one to see the strongest answer and why it works.
A teammate calls their test a unit test, but it starts a database container and an HTTP server. Does the label matter?
The strongest answer: Yes — a unit test exercises one behaviour with no database, network, clock or filesystem, so it is fast and deterministic; what they have is an integration test, which is fine but belongs in a slower stage
Why it works: Exactly — the label sets the expectation: runs on every save, deterministic, and never red because something else was down.
- Unit: one behaviour, no I/O.
- Fast and deterministic by definition.
- Integration tests are fine — elsewhere.
- Name the layer honestly.
In a test, what is the difference between a stub, a mock and a fake?
The strongest answer: A stub returns canned answers so the code under test can run; a mock also records calls so the test can assert an interaction happened; a fake is a real but lightweight implementation, such as an in-memory repository
Why it works: Right — and the practical point is that asserting on a mock ties the test to how the code works, not to what it does.
- Stub: canned answers.
- Mock: asserts an interaction.
- Fake: a working lightweight version.
- Never mock the code under test.
Your repository tests run against an in-memory database and pass, but the same query fails in production. What do you change?
The strongest answer: Test against the engine you ship on, in a throwaway container per run — the in-memory substitute has different SQL, types and constraint behaviour — and have each test set up and clean its own data
Why it works: Exactly — same engine, disposable instance, isolated data. Anything else and you are testing a different database.
- Test on the engine you ship.
- Disposable instance per run.
- Isolate the data per test.
- In-memory is a different database.
How do you test that POST /orders returns 201 with the right body, without deploying anything?
The strongest answer: Start the application inside the test process with its real routing, serialisation and middleware, send the request through the framework’s test client, and stub only the external dependencies at the boundary
Why it works: Right — that path covers routing, model binding, validation and serialisation, which is where the status code and the body are actually decided.
- Boot the app in the test process.
- Go through routing and middleware.
- Stub only external boundaries.
- Assert status, headers and body.
A refactor that changed no behaviour broke forty tests. Each one asserts that a particular method was called on a mock.
What is the lesson?
The strongest answer: The tests were pinned to the implementation rather than the behaviour — mock only what crosses a boundary you do not own, and assert on outcomes, so a refactor that keeps the contract keeps them green
Why it works: Exactly — a test that goes red on a safe refactor is a maintenance bill, not a safety net.
- Assert behaviour, not calls.
- Mock at boundaries you do not own.
- A safe refactor should stay green.
- Brittle tests are a maintenance bill.
Three teams call your service. Twice this quarter one of your releases broke one of them, and nobody noticed until their alerts fired.
What would consumer-driven contract tests give you here?
The strongest answer: Each consumer publishes the requests it makes and the response fields it relies on, and your pipeline verifies every published contract before release — so CI tells you which consumer you would break, and which fields nobody uses
Why it works: Exactly — and the second half matters: contracts show which fields nobody relies on, which is how you get to delete things safely.
- Consumers publish what they rely on.
- Verify every contract before release.
- Break in CI, not in their alerts.
- Unused fields become safe to remove.
The service’s CI suite fails about one run in five for reasons unrelated to the change, and the team’s habit is to press rerun.
What do you put in place?
The strongest answer: Treat a flake as a defect with an owner and a deadline: quarantine it out of the blocking suite so the signal comes back, track the flake rate, and fix or delete the quarantined tests on a clock
Why it works: Exactly — the failure mode is a quarantine list nobody empties, so the owner and the date are the policy, not the quarantine itself.
- A flake is a defect.
- Quarantine to restore the signal.
- Give each one an owner and a date.
- Track the rate, not the excuses.