Skip to content
C Codeloom
Testing

Mocking and Stubbing Strategies in Tests

Pick the right test double for the job. Compare mocks, stubs, fakes, and spies, and learn when to skip them entirely.

·4 min read · By Codeloom
Intermediate 8 min read

What you'll learn

  • The difference between mocks, stubs, fakes, and spies
  • When to mock and when to use the real thing
  • How to mock HTTP boundaries cleanly
  • Why overmocking hurts
  • How to evolve your test doubles over time

Prerequisites

  • Comfortable writing basic unit tests

What and Why

Tests need to control inputs and isolate the unit under test. Test doubles let us swap real collaborators for controllable stand-ins. Picking the right kind keeps tests fast and meaningful, while choosing poorly leads to brittle, low-value test suites.

Mental Model

Test doubles come in four flavors that get confused often.

  • Stub: returns canned data. No assertions.
  • Mock: records calls and asserts on them.
  • Fake: a working but simplified implementation, like an in-memory database.
  • Spy: a real object wrapped so you can observe calls without changing behavior.
Need controlled input?  --> Stub
Need to verify interaction? --> Mock or Spy
Need realistic behavior offline? --> Fake
Don't actually need isolation? --> Use the real collaborator
Choosing a test double

Hands-on Example

A function that emails users about new orders.

// notifier.js
export class Notifier {
  constructor({ emailClient, db }) {
    this.emailClient = emailClient;
    this.db = db;
  }
  async notifyOrder(orderId) {
    const order = await this.db.orders.get(orderId);
    if (!order) return false;
    await this.emailClient.send(order.userEmail, "Your order", `Total: ${order.total}`);
    return true;
  }
}

Stub the database, mock the email client.

import { test, expect, vi } from "vitest";
import { Notifier } from "./notifier.js";

test("notifies user via email", async () => {
  const db = { orders: { get: async () => ({ id: 1, userEmail: "a@x.io", total: 42 }) } };
  const emailClient = { send: vi.fn().mockResolvedValue(true) };

  const n = new Notifier({ db, emailClient });
  const ok = await n.notifyOrder(1);

  expect(ok).toBe(true);
  expect(emailClient.send).toHaveBeenCalledWith("a@x.io", "Your order", "Total: 42");
});

The db is a stub, no assertions on it. The emailClient is a mock; we verify how it was called.

For HTTP calls, mock at the network boundary, not the client library.

import { setupServer } from "msw/node";
import { http, HttpResponse } from "msw";

const server = setupServer(
  http.get("https://api.example.com/user/1", () =>
    HttpResponse.json({ id: 1, name: "Ada" })),
);

beforeAll(() => server.listen());
afterAll(() => server.close());

A fake makes integration tests realistic without hitting the network.

class InMemoryUserRepo {
  constructor() { this.rows = new Map(); }
  async save(u) { this.rows.set(u.id, u); return u; }
  async byId(id) { return this.rows.get(id) ?? null; }
}

Common Pitfalls

  • Mocking everything. Tests pass even when the real code is broken because the doubles never reflected reality.
  • Asserting too much about call shape. Hard-coding argument order makes refactors painful for no real safety.
  • Mocking what you do not own. If you mock a third-party library’s internals, every upgrade breaks tests.
  • Replacing the database in integration tests. Use a real DB or an in-memory fake instead of a half-mock.
  • Forgetting cleanup. Stale mocks leak between tests and cause flakes that surface days later.

Practical Tips

  • Mock at architectural seams: HTTP, database, time. Avoid mocking your own pure functions.
  • Use vi.fn() or jest.fn() to combine stub and mock when you need both control and observation.
  • Wrap external services behind a small interface in your code. Tests can swap the implementation easily.
  • Lean on tools like msw for HTTP, nock for Node, and httpx mocking or respx for Python.
  • Promote stubs to fakes when they grow logic. Fakes are easier to maintain than ever-expanding stub maps.

Wrap-up

Mocks, stubs, fakes, and spies are different tools for different jobs. Stubs feed inputs, mocks verify interactions, fakes mimic behavior, and spies watch without interfering. Use them where they earn their keep, prefer real collaborators when speed and isolation are not at risk, and your test suite will grow into a confident safety net instead of a maintenance burden.