Automation

Test doubles: mock, stub, spy, fake and dummy

"What's the difference between a mock and a stub?" is one of those technical questions that best separates people who memorize from people who understand. In day-to-day practice, every unit test isolates the piece under test from its dependencies — database, external API, clock, email — and does so with test doubles. The umbrella term comes from Gerard Meszaros, and there are five kinds, from the dumbest to the smartest.

The spectrum of doubles

  just takes up space ──────────────────────► verifies interaction
   DUMMY → STUB → FAKE → SPY → MOCK

   DUMMY  passed around but never used (fills a parameter)
   STUB   returns canned answers ("when asked X, answer Y")
   FAKE   a real implementation, but simplified (e.g. in-memory database)
   SPY    like a stub, but records how it was called (to check later)
   MOCK   comes with the expectation of what MUST be called (fails if not)

Dummy — just to fill the slot

An object passed only to satisfy the signature, never actually used on the tested path. E.g. a logger required by the constructor, but irrelevant to what I'm verifying.

const loggerDummy = {};
const servico = new Servico(loggerDummy); // just to build it

Stub — canned answers

The stub controls indirect input: it forces the dependency to return a fixed value, so I can test how my code reacts. I don't verify how it was called — I just use the answer.

// I want to test the free-shipping rule without calling the real ZIP code API
const apiCepStub = {
  consultar: () => ({ regiao: 'sudeste', taxa: 0 })
};

const total = calcularEntrega(apiCepStub, carrinho);
expect(total.freteGratis).toBe(true); // I verify STATE

Fake — a real but lightweight implementation

The fake has real logic, just simplified and unsuitable for production. The classic example is a repository that keeps data in an in-memory array instead of hitting the database — fast and with no side effects.

class RepositorioFake {
  constructor() { this.itens = []; }
  salvar(item) { this.itens.push(item); }
  buscarPorId(id) { return this.itens.find(i => i.id === id); }
}

Spy — records what happened

The spy wraps a call and records how it was used — how many times, with which arguments — so I can inspect it after the action. It's the middle ground: it sets no expectation up front, but lets me verify behavior.

const emailSpy = jest.fn();
notificarUsuario(usuario, emailSpy);

// I verify BEHAVIOR: was it called correctly?
expect(emailSpy).toHaveBeenCalledTimes(1);
expect(emailSpy).toHaveBeenCalledWith('ana@exemplo.com');

Mock — built-in expectation

The mock is the most opinionated: you define up front what it expects to receive, and the mock itself fails the test if the expected interaction doesn't happen. It verifies behavior by construction.

const gateway = mock(PagamentoGateway);
when(gateway.cobrar(100)).thenReturn({ ok: true });

finalizarCompra(gateway, 100);

// the mock requires that cobrar(100) was called exactly like this
verify(gateway).cobrar(100);

The distinction that matters: state vs behavior

More useful than memorizing the five names is understanding the split they reveal:

Verifies…Typical doubleThe test's question
State (result)Stub, Fake"Given this return value, did my code produce the right output?"
Behavior (interaction)Spy, Mock"Did my code call the dependency the way I expected?"
A stub answers, a mock demands. A stub serves a state verification; a mock is the verification — of behavior. Confusing the two leads to tests that verify the wrong thing.

Beware of too many mocks

Too many doubles couple the test to the implementation: you end up testing "method X called method Y" instead of observable behavior. Then any refactoring breaks the tests even without a bug. The practical rule is to double what is slow, unstable or has side effects (network, database, time, email) and let the business logic run for real.

Conclusion

A dummy fills, a stub answers, a fake implements lightly, a spy records and a mock demands. Knowing which one to use isn't trivia — it's what keeps your unit tests fast, isolated and focused on what really matters: the behavior of the code, not the details of whom it calls.

Read also

Back to the blog