SoftwareTestPilot
Topic 9 of 100

Mocking — Definition, Doubles Taxonomy & Best Practices

A mock is a programmable substitute for a real dependency that also verifies how it was called. Overused, mocks freeze bad designs; used well, they turn slow tests into fast, focused ones.

Last updated: June 2026

Section 1

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.

Section 2

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 Postgres

Dummies 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.

typescript
src/orders/create.spec.ts (Vitest)
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();
  });
});
Section 3

Mock vs Stub vs Spy vs Fake

DoubleProvides answers?Records calls?Fails if misused?Typical use
DummyNoNoNoFiller argument
StubYes (canned)NoNoIsolate from slow deps
SpyYes (canned)YesNo (assert after)Verify a call happened
MockYes (canned)YesYes (upfront expectations)Verify a protocol
FakeYes (working)N/AN/AIn-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.

Section 4

Production Debugging Scenarios

Mocking pathologies rarely fail the test suite — they slow future refactors. Watch for these three patterns during code review.

Scenario 1

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.
Scenario 2

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.
Scenario 3

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.

People Also Ask

1.What is a mock in software testing?
A programmable substitute for a real dependency that also verifies how the code under test interacted with it.
2.Are mocks and stubs the same thing?
No. A stub provides canned responses; a mock provides responses AND verifies the interaction.
3.When should I use a mock instead of a real dependency?
When the real dependency is slow, non-deterministic, or hard to invoke — networks, payment providers, email services.
4.What is over-mocking?
Replacing internal collaborators with mocks such that tests break on any refactor even without behaviour changes.
5.Should I mock the database?
Prefer an in-memory fake (SQLite, testcontainers) over a mocked ORM. Mocks freeze the query shape; fakes let you refactor freely.
6.Is MSW a mock?
MSW is a stub at the network layer — it returns canned responses to intercepted HTTP calls without recording expectations upfront.
7.What is the difference between mocks and fakes?
Fakes have working (simplified) implementations; mocks have no real behaviour, only recorded interactions.
8.Do I need a mocking library?
For TypeScript, vi.fn() covers most cases. For Java, Mockito is standard. For Python, unittest.mock is built in.
9.Are mocks used in TDD?
Yes — mocks are common when a collaborator is not yet built. This is the 'mock roles, not objects' pattern from Freeman & Pryce.