What are mocks, stubs, and spies, and when should you use them?

3 minintermediatenodejsmockingstubsspiestest-doubles

Quick Answer

Test doubles replace real dependencies so a unit test stays isolated and fast. A spy records how a function was called; a stub replaces it with canned behavior/return values; a mock is a stub with built-in expectations about how it should be called. Use them to isolate the unit under test and avoid slow or non-deterministic dependencies (DB, network, time).

Detailed Answer

Answer: Test doubles stand in for real dependencies so you can test a unit in isolation, deterministically, without hitting databases, networks, or the clock.

The three common kinds:

  • Spy — wraps a real (or empty) function and records calls (arguments, call count) without changing behavior. Use to assert "was this called correctly?"
  • Stub — replaces a function with a canned implementation/return value. Use to control what a dependency returns (e.g., simulate a DB result or an error).
  • Mock — a stub with pre-set expectations that the test verifies (it should be called once, with these args).

In Jest:

// Spy — observe an existing method
const logSpy = jest.spyOn(console, 'log');
doWork();
expect(logSpy).toHaveBeenCalledWith('done');

// Stub/mock a function's return value
const getUser = jest.fn().mockResolvedValue({ id: 1, name: 'Alice' });

// Mock an entire module
jest.mock('./emailClient');
const { sendEmail } = require('./emailClient');
sendEmail.mockResolvedValue({ ok: true });

test('notifies the user', async () => {
  await notify(1);
  expect(sendEmail).toHaveBeenCalledWith('alice@x.com', expect.any(String));
});

When to use — and when not to:

  • Do mock: slow/external dependencies (DB, HTTP, payment APIs), non-determinism (time with fake timers, randomness), and to force error paths that are hard to trigger for real.
  • Don't over-mock: mocking everything tests your mocks, not your code, and makes tests brittle to refactors. Prefer real objects for pure logic, and cover real wiring with integration tests.

Rule: mock at the boundaries (I/O), keep the core logic tested against the real thing.