Executive Definition
A mock is a test double — a stand-in for a real dependency — that both replaces the collaborator's behaviour and verifies how the code under test interacted with it. The term was formalised by Gerard Meszaros in xUnit Test Patterns (2007), which introduced a taxonomy of five doubles: dummies, fakes, stubs, spies, and mocks.
The distinguishing feature of a mock is behavioural verification: after the test runs, you assert that the mock was called with the expected arguments, in the expected order, the expected number of times. A stub, by contrast, only provides canned responses — it does not care how it was invoked. This distinction matters because mocks tie tests to the interaction protocol of the code under test, which is powerful but brittle.
Mocking exists to solve three real problems. First, real dependencies are slow — a database call takes milliseconds, a network call takes hundreds; mocking replaces them with in-memory substitutes that run in microseconds. Second, real dependencies are unreliable — external APIs go down, third-party services rate-limit; mocking removes those failure modes from your test outcomes. Third, some collaborators are impossible to invoke deterministically — payment providers, SMS gateways, email services; mocking lets you assert 'we would have sent this' without actually sending it.
The classic anti-pattern is over-mocking — replacing every collaborator with a mock, then writing tests that verify implementation details rather than behaviour. A test that mocks the database, mocks the cache, mocks the logger, and asserts each was called in exact sequence proves only that the code calls what it currently calls. Any refactor that improves the design breaks the tests without changing behaviour.
The rule of thumb in 2026 is: mock at the seams, not inside the modules. Mock the HTTP client at the boundary of your service so tests do not hit real networks; do not mock the internal functions your module composes to serve that HTTP call. Frameworks like MSW (Mock Service Worker) have made this pattern easier by intercepting at the network layer, which is a cleaner seam than mocking function-by-function.
Architecture & Production Code
The Meszaros taxonomy is the reference vocabulary. Understanding where each double sits on the spectrum prevents confusion during code review.
NO BEHAVIOUR FULL BEHAVIOUR
│ │
Dummy ──────── Stub ──────── Spy ──────── Mock ──────── Fake
(unused) (canned (canned + (canned + (working
answers) recording) verification) minimal impl)
Example dummies: a null Logger passed to satisfy a constructor
Example stubs: an HTTP client that returns fixed JSON
Example spies: a stub that also records every call for later inspection
Example mocks: a stub that fails the test if uncalled or misused
Example fakes: in-memory SQLite standing in for PostgresDummies pass through argument lists but are never invoked. Stubs answer questions with prepared responses. Spies do the same and remember how they were called. Mocks add expectations up front — 'you must call me with these arguments, exactly twice' — and fail the test if the expectation is not met.
Fakes are the interesting outlier. A fake has a working implementation, just a simpler or faster one — an in-memory SQLite standing in for Postgres, an in-memory queue standing in for Kafka. Fakes give you real behaviour without the operational cost.
In practice, most 'mocks' in modern TypeScript codebases are actually stubs or spies (vi.fn returns a spy by default). The vocabulary matters because mock everything and stub everything have very different consequences during refactor — mocks break, stubs usually survive.
import { describe, it, expect, vi, beforeEach } from "vitest";
import { createOrder } from "./create";
describe("createOrder", () => {
const paymentClient = { charge: vi.fn() };
const emailer = { send: vi.fn() };
const db = { insert: vi.fn().mockResolvedValue({ id: 42 }) };
beforeEach(() => vi.clearAllMocks());
it("charges the card, inserts the order, and emails a receipt", async () => {
paymentClient.charge.mockResolvedValue({ ok: true, txnId: "t_1" });
const order = await createOrder(
{ userId: "u_1", cart: [{ sku: "abc", qty: 1, price: 100 }] },
{ db, paymentClient, emailer },
);
expect(order).toMatchObject({ id: 42, status: "paid" });
expect(paymentClient.charge).toHaveBeenCalledWith({ amount: 100, userId: "u_1" });
expect(db.insert).toHaveBeenCalledOnce();
expect(emailer.send).toHaveBeenCalledWith(expect.objectContaining({ to: "u_1" }));
});
it("does not insert or email when the charge fails", async () => {
paymentClient.charge.mockResolvedValue({ ok: false, error: "declined" });
await expect(
createOrder({ userId: "u_1", cart: [{ sku: "abc", qty: 1, price: 100 }] },
{ db, paymentClient, emailer }),
).rejects.toThrow("payment declined");
expect(db.insert).not.toHaveBeenCalled();
expect(emailer.send).not.toHaveBeenCalled();
});
});Mock vs Stub vs Spy vs Fake
| Double | Provides answers? | Records calls? | Fails if misused? | Typical use |
|---|---|---|---|---|
| Dummy | No | No | No | Filler argument |
| Stub | Yes (canned) | No | No | Isolate from slow deps |
| Spy | Yes (canned) | Yes | No (assert after) | Verify a call happened |
| Mock | Yes (canned) | Yes | Yes (upfront expectations) | Verify a protocol |
| Fake | Yes (working) | N/A | N/A | In-memory DB / queue |
Most 'mock everything' tests are actually stub- or spy-heavy. That is usually fine. Reserve true behavioural mocks — with upfront expectations — for verifying command protocols that must be exact, like billing or state-machine transitions.
Production Debugging Scenarios
Mocking pathologies rarely fail the test suite — they slow future refactors. Watch for these three patterns during code review.
Renaming an internal method breaks 40 tests
- Symptom
- A pure refactor with no behaviour change turns the suite red.
- Root cause
- Tests mock the internal method by name rather than the boundary of the module.
- Fix
- Move mocks out to the module seam (HTTP client, DB adapter). Delete internal-function mocks; rely on real composition instead.
Test mocks pass; production fails on real dependency
- Symptom
- createOrder passes tests; live orders fail with 'invalid amount' at the payment provider.
- Root cause
- The mock returned the shape the test expected, not the shape the real provider requires.
- Fix
- Generate the mock from the real provider's OpenAPI schema, or run a nightly integration test against a sandbox environment.
Mock leak between tests causes ghost failures
- Symptom
- Tests pass when run in isolation, fail when run in a file after another test.
- Root cause
- vi.mock() persisted between tests without a reset; the second test sees the first test's return value.
- Fix
- Call vi.restoreAllMocks() (or beforeEach reset) and prefer vi.fn() per-test over module-level mocks.
Practice this concept in a real QA interview
Run a live mock with our AI Interview Coach, tune your resume with the ATS Resume Reviewer, and screen live listings on the QA Jobs Radar.