Skip to content

Jest Test Doubles

"Mock" is often used loosely to describe any fake dependency, but there's a more precise vocabulary borrowed from classic testing literature. This lesson introduces test doubles as a category and maps each type to Jest's tools.

The Five Types of Test Doubles

A "test double" is the general term (coined by Gerard Meszaros) for any object that stands in for a real dependency during a test. The five commonly recognized types — dummy, stub, spy, mock, and fake — differ in how much behavior they simulate and how you use them to verify test outcomes.

Jest's tooling doesn't force this vocabulary on you, but understanding the distinctions helps you reach for the right level of fakery instead of always defaulting to a full mock.

// Dummy: passed but never actually used
function createUser(name, logger = null) { /* logger unused here */ }

// Stub: returns canned data, not checked for calls
const stubRepo = { findById: () => ({ id: 1, name: 'Ada' }) };

// Spy: real behavior, tracked calls
const spy = jest.spyOn(calculator, 'add');

// Mock: fake behavior AND checked for calls
const mockSend = jest.fn();

// Fake: a working, simplified implementation
class InMemoryUserRepo {
  constructor() { this.users = []; }
  save(user) { this.users.push(user); }
}

All five patterns are "fakes" in casual conversation, but each serves a distinct testing purpose.

Test Double Vocabulary at a Glance

Dummy — passed to satisfy a signature, never actually used
Stub  — returns fixed/canned data, not verified for calls
Spy   — wraps real behavior, tracked for calls
Mock  — fake behavior, explicitly verified for calls/arguments
Fake  — a simplified but genuinely working implementation
  • Dummies exist purely to satisfy a function signature (e.g. a required but unused callback argument).
  • Stubs provide canned answers; you assert on the code's *result*, not on the stub's calls.
  • Spies and mocks both track calls, but a mock's whole purpose is being verified, while a spy is often observational.
  • Fakes (like an in-memory repository) behave close to the real thing, useful when a stub is too simplistic.

Test Doubles Cheat Sheet

Mapping test double vocabulary to Jest tools.

Test Double Jest Equivalent Verified By
Dummy A plain unused object/null Not verified
Stub jest.fn().mockReturnValue(x) The result of the code under test
Spy jest.spyOn(obj, 'method') Call tracking, real behavior preserved
Mock jest.fn() + toHaveBeenCalledWith() Explicit call/argument verification
Fake A small in-memory implementation class Behavior matches real usage

The Stub vs. Mock Distinction in Practice

The clearest practical distinction: a stub just feeds data into the system under test so you can check the *outcome*; a mock is itself the subject of an assertion ("was send() called with these exact arguments?"). The same jest.fn() can act as either, depending on whether your test asserts on it directly.

const emailClient = { send: jest.fn().mockResolvedValue(true) };

// Used as a stub: we only check the function's own return value
const result = await notifyUser(user, emailClient);
expect(result).toBe(true);

// Used as a mock: we assert directly on how it was called
expect(emailClient.send).toHaveBeenCalledWith(user.email, expect.any(String));

When a Fake Beats a Stub

A fake in-memory repository (that actually stores and retrieves data, just not in a real database) can catch bugs a simple stub would miss — like accidentally saving the same record twice — while remaining much faster than a real database connection.

class InMemoryUserRepository {
  constructor() { this.users = new Map(); }
  save(user) { this.users.set(user.id, user); }
  findById(id) { return this.users.get(id) ?? null; }
}

test('does not overwrite an existing user with the same id', () => {
  const repo = new InMemoryUserRepository();
  repo.save({ id: 1, name: 'Ada' });
  repo.save({ id: 1, name: 'Grace' });

  expect(repo.findById(1).name).toBe('Grace'); // reveals real save/overwrite behavior
});

Common Mistakes

  • Calling every kind of test double a "mock" regardless of how it's actually used in the test.
  • Using a stub when the test actually needs to verify specific calls (should be a mock).
  • Reaching for a full mock when a simple, canned stub value would be simpler and sufficient.
  • Never considering a lightweight fake for dependencies where a stub is too simplistic to catch real bugs.

Key Takeaways

  • Test doubles is the umbrella term; dummy, stub, spy, mock, and fake are specific types within it.
  • Stubs feed data in; mocks are directly verified for how they were called.
  • Jest's jest.fn() can act as a stub or a mock depending on how the test uses it.
  • Fakes (simplified working implementations) can catch bugs that simple stubs miss.

Pro Tip

Before writing a test double, ask: "am I checking the *result* of using this fake, or the *fact that it was called correctly*?" The answer tells you whether you're really writing a stub or a mock, even if you reach for the same jest.fn() either way.