SoftwareTestPilot
Topic 10 of 100

Stubbing — Definition, Patterns & When to Prefer It Over Mocks

A stub is the quietest test double — it answers questions with prepared responses and asks nothing in return. That silence is exactly why it is safer than a mock for the majority of tests.

Last updated: June 2026

Section 1

Executive Definition

A stub is a test double that provides canned responses to calls made during a test. Unlike a mock, a stub does not verify how it was called; it exists purely to isolate the code under test from a real collaborator that would otherwise be slow, unreliable, or non-deterministic.

The vocabulary comes from Gerard Meszaros's xUnit Test Patterns. Stubs sit next to dummies, spies, mocks, and fakes on the doubles spectrum. The defining property of a stub is behavioural neutrality — it answers, but it does not judge. That neutrality makes stubs safer than mocks during refactors: renaming or restructuring the code under test typically does not break a stub, because the stub was never asserting on the interaction.

Stubs address a specific problem: state-based tests need controlled inputs. If the code under test asks a repository for a user, the test needs the repository to return a specific user shape. A stub delivers that user shape without touching a real database. The assertion in the test then focuses on what the code did with the user — not on the fact that the repository was called.

Stubbing is the correct default for the majority of unit tests. Reach for a mock only when the interaction itself is the behaviour under test — for example, verifying that a billing service invokes the payment provider with the exact amount and idempotency key. For most tests — 'does this reducer produce the right state, given this input?' — a stub is enough and a mock is overkill.

In 2026 the practical distinction between stubs and mocks in TypeScript codebases has blurred, because vi.fn() and jest.fn() return spies that can serve as either. The vocabulary still matters because it clarifies intent during code review — 'this is a stub because we do not care how many times it was called; assert only on the outcome' is a useful sentence to have in the shared toolbox.

Section 2

Architecture & Production Code

Stubs live at the boundary between the code under test and everything else. The cleaner the boundary, the safer the stub.

                       ┌────────────────────┐
                       │   Code under test   │
                       └─────────┬──────────┘
                                 │  depends on
                                 ▼
                       ┌────────────────────┐
   In production ──▶   │   Real dependency  │
                       │   (DB, HTTP, SDK)  │
                       └────────────────────┘

                       ┌────────────────────┐
   In test ──────────▶ │   Stub             │
                       │   Returns canned   │
                       │   responses only   │
                       └────────────────────┘

       Test asserts on the CODE UNDER TEST's output — not on the stub's calls.

The diagram makes the seam explicit. In production, the code under test collaborates with a real dependency. In test, the same collaborator slot is filled by a stub that hands back predetermined answers. Nothing inside the code under test changes between the two configurations.

The test asserts on the output of the code under test, not on the stub. That is the discipline that keeps stub-based tests robust. Once the assertion moves to 'the stub was called', you have implicitly promoted the stub to a mock and inherited the refactor risk that goes with it.

Choosing where to place the seam is a design decision. Stubbing at the HTTP-client boundary (or the repository interface) tends to age well. Stubbing at individual internal function boundaries tends to age badly, because any refactor that splits or merges functions requires re-authoring the stub.

typescript
src/checkout/computeTotal.spec.ts (Vitest stubs)
import { describe, it, expect, vi } from "vitest";
import { computeTotal } from "./computeTotal";

describe("computeTotal", () => {
  it("applies the tax rate returned by the geo service", async () => {
    // Stub — returns a canned value, no expectations
    const geoService = {
      getTaxRate: vi.fn().mockResolvedValue(0.19),
    };

    const total = await computeTotal(
      { subtotal: 100, country: "DE" },
      { geoService },
    );

    // Assertion is on the OUTPUT, not on the stub
    expect(total).toBe(119);
  });

  it("falls back to 0% tax when the geo service fails", async () => {
    const geoService = {
      getTaxRate: vi.fn().mockRejectedValue(new Error("timeout")),
    };

    const total = await computeTotal(
      { subtotal: 100, country: "ZZ" },
      { geoService },
    );

    expect(total).toBe(100);
    // Note: we do NOT assert getTaxRate was called — that is a mock's job.
  });
});

// Equivalent Sinon-style stubbing:
//   const geo = { getTaxRate: sinon.stub().resolves(0.19) };
Section 3

Stub vs Mock vs Fake

AspectStubMockFake
Provides canned responses?YesYesYes (via real logic)
Verifies interactions?NoYesNo
Fails on misuse?NoYes (upfront expectations)Only on real bugs
Refactor safetyHighLowHigh
Best whenYou care about outputsYou care about the protocolYou need real-ish behaviour
Typical examplegeoService.getTaxRate stubpaymentClient.charge mockIn-memory SQLite fake

The choice between stub and mock is a choice about what the test is really claiming. If the claim is 'the module produced the correct output', use a stub. If the claim is 'the module invoked the collaborator with the correct protocol', use a mock. Choosing the wrong double buys you brittle tests or missed defects.

Section 4

Production Debugging Scenarios

Stubs fail quietly. The suite stays green while confidence drains away. Watch for these three patterns.

Scenario 1

Stubbed dependency returns a shape the real one no longer produces

Symptom
Tests are green; production crashes with 'cannot read property currency of undefined'.
Root cause
The real API added a required field; the stub was not updated.
Fix
Generate the stub payload from a schema (OpenAPI, Zod) shared with production. Add a nightly contract test against the real dependency.
Scenario 2

Stub leaks between tests via module-level state

Symptom
The second test in a file sees the first test's stub return value.
Root cause
vi.mock was declared at module scope without a reset.
Fix
Prefer per-test stubbing with vi.fn() and call vi.restoreAllMocks() in afterEach.
Scenario 3

Stub-heavy suite blocks a legitimate refactor

Symptom
Splitting a class into two smaller ones requires updating 60 stubs.
Root cause
Stubs were placed at internal function boundaries rather than at the module seam.
Fix
Move stubs out to the HTTP or repository seam. Replace internal-function stubs with real composition, exercised end-to-end within the unit test.

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 stubbing in software testing?
Replacing a real dependency with a substitute that returns canned responses, without verifying how it was called.
2.How is stubbing different from mocking?
Stubs only answer; mocks answer AND verify the interaction protocol. Stubs are safer across refactors.
3.When should I stub instead of mock?
Whenever the test's claim is about output, not about the protocol used to reach that output — which covers most unit tests.
4.Can I stub a database?
Yes, but an in-memory fake (SQLite, testcontainers) is usually better — it exercises real query logic while staying fast.
5.Do stubs slow down refactors?
Only when placed at internal function boundaries. Stubs at the module seam (HTTP, repository) age well.
6.Is Sinon a stubbing library?
Yes — Sinon.stub() creates canned-response doubles. It also provides mocks and spies for the other double types.
7.What is the difference between a stub and a fake?
A stub returns canned values; a fake has a simplified but working implementation.
8.Should I stub the logger?
Usually leave the logger real — it is fast and its side effects are harmless in tests. Stub only if the test would produce misleading log noise.
9.Can stubs be used in integration tests?
Sparingly. Integration tests exist to exercise real collaborators; stub only the true externalities (payment, email, SMS).