BugMojoBugMojoBugMojo
FeaturesPricingBlogHelpAbout
Add to ChromeLog inGet started
BugMojoBugMojo

Bug reports that actually help fix bugs — capture, replay, share.

A product of Softech Infra.

Product

  • Features
  • Pricing
  • Browser extension
  • Get started
  • Log in

Resources

  • Help & guides
  • Blog
  • Compare
  • Glossary

Company

  • About
  • Contact
  • Security
  • Privacy
  • Terms
  • Sitemap
© 2026 BugMojo. All rights reserved.
AllGuidesEngineeringPlaybooksCompareGlossaryAlternativesBy roleBug tracking by framework
  1. Home
  2. Blog
  3. Guides
  4. Mock vs Stub vs Spy: Test Doubles Explained
Guide

Mock vs Stub vs Spy: Test Doubles Explained

Mock, stub, spy, fake, dummy — the words get used interchangeably and it costs you clarity in code review. Here is Gerard Meszaros's precise taxonomy, the state-versus-behavior split Martin Fowler drew, and when each one earns its place.

ManviManvi·Jul 16, 2026·9 min read
Guides
Isometric line-art hub of test doubles — mock, stub, spy, fake, dummy — ringing a central system-under-test node, lime on dark charcoal
TL;DR
  • Test double is the umbrella term (Gerard Meszaros) for any stand-in you swap for a real dependency in a test.
  • Dummy fills a parameter but is never used. Stub returns canned answers (indirect input). Spy is a stub that also records calls. Mock carries expectations and self-verifies. Fake is a working but simplified implementation.
  • The one split that matters: stub/fake enable state verification (check the result); spy/mock enable behavior verification (check the interactions).
  • Watch out: over-mocking couples tests to implementation — they pass while the real integration breaks for users.

Almost everyone calls every test double a "mock." It is a harmless habit right up until a code review where one person means "an object that returns a fixed value" and another means "an object that fails the test if it isn't called." Those are different tools with different consequences, and the sloppy vocabulary hides real design decisions.

The good news is the terminology was pinned down years ago. Gerard Meszaros gave us the umbrella term test double and five named categories in xUnit Test Patterns, and Martin Fowler popularized the distinctions in his essay Mocks Aren't Stubs. This guide walks the five, each with a code example, then draws the one conceptual line that actually changes how you test.

What is a test double?

A test double is any object that stands in for a real dependency during a test, the way a stunt double replaces an actor. Gerard Meszaros coined the term and split it into five kinds — dummy, stub, spy, mock, and fake — which differ by how much behavior they carry and whether they verify state or interactions.

You reach for a double when the real collaborator is slow, non-deterministic, expensive, hard to set up, or simply not the thing under test — a payment gateway, a clock, a mailer, a database. "Test double" is the generic word; mock is one specific member of the family, not a synonym for the whole thing. Meszaros's five differ along two axes: how much real behavior they implement, and whether they exist to feed the code inputs or to observe its outputs. Keep those two questions in mind as we go.

The five test doubles

Dummy — fills a slot, never used

A dummy is the emptiest double. It is passed around to satisfy a parameter list but its value is never actually exercised on the path you are testing. It exists purely to let you construct the object under test.

dummy.test.jsjavascript
// The constructor requires a logger, but this test path never logs.
// We pass a dummy just to satisfy the signature.
const dummyLogger = {};

const cart = new ShoppingCart(dummyLogger);
cart.addItem({ id: 'sku-1', price: 10 });

expect(cart.total()).toBe(10); // logger never touched

Stub — canned answers (indirect input)

A stub is pre-programmed to return fixed, canned answers to the calls made during the test. Its job is to provide indirect input — to feed the code under test whatever it needs from a collaborator so you can drive a specific path. The ISTQB glossary defines it as a skeletal implementation used to supply the caller with required responses. You never assert on the stub itself; you assert on what the code does with its answers.

stub.test.jsjavascript
// Stub returns a canned exchange rate so we can test conversion logic
// without hitting a live currency API.
const rateStub = {
  getRate: () => 0.9, // always returns this, regardless of args
};

const converter = new PriceConverter(rateStub);

// We verify the RESULT (state), not how the stub was called.
expect(converter.toEur(100)).toBe(90);

Spy — a stub that records how it was called

A spy is a stub with a notebook. It still returns canned answers, but it also records how it was called — which methods, with what arguments, how many times. You inspect that record after the fact to assert the interaction happened. The verification is retrospective: exercise the code first, then check what the spy captured.

spy.test.jsjavascript
// Spy records every call so we can assert on it afterward.
const emailSpy = {
  calls: [],
  send(to, subject) {
    this.calls.push({ to, subject }); // record
    return true;                       // canned answer
  },
};

const service = new SignupService(emailSpy);
service.register('ada@example.com');

// Verification happens AFTER the fact, on the recorded calls.
expect(emailSpy.calls).toHaveLength(1);
expect(emailSpy.calls[0].to).toBe('ada@example.com');

Mock — expectations that self-verify

A mock is pre-programmed with expectations: a specification of exactly which calls it should receive, with which arguments, in what order. The mock verifies behavior — if the expected interaction never happens, the mock itself fails the test. This is the key difference from a spy: a spy passively records and lets you assert later; a mock actively carries the assertion and enforces it. Fowler's essay uses this behavior-verifying object as the definition of a true mock.

mock.test.jsjavascript
// Mock is set up with an EXPECTATION before the code runs.
const mailer = mock(Mailer);
mailer
  .expects('send')
  .once()
  .withArgs('ada@example.com', 'Welcome');

const service = new SignupService(mailer);
service.register('ada@example.com');

// The mock verifies the expectation itself — this FAILS the test
// if send() was never called with those exact args.
mailer.verify();

Fake — a working, simplified implementation

A fake has real, working behavior — just a shortcut version not fit for production. The classic example is an in-memory database that genuinely stores and queries data but keeps it in a map instead of on disk. A fake actually does the thing, which is what separates it from a stub's canned answers. You still verify state — you inspect the fake's contents or the result — but the logic in between is real.

fake.test.jsjavascript
// Fake: a real, working repository backed by a Map instead of Postgres.
class InMemoryUserRepo {
  #users = new Map();
  save(user) { this.#users.set(user.id, user); }
  findById(id) { return this.#users.get(id); }
}

const repo = new InMemoryUserRepo();
const service = new UserService(repo);
service.createUser({ id: 'u1', name: 'Ada' });

// State verification against a fake that genuinely persisted the data.
expect(repo.findById('u1').name).toBe('Ada');

The one split that matters: state vs behavior

If you remember one thing, make it this. Fowler's central insight in Mocks Aren't Stubs is that the doubles cluster into two styles of verification:

  • State verification — exercise the code, then check the result: a return value or the state of an object afterward. Stubs and fakes support this style; they supply inputs and stay out of the way.
  • Behavior verification — check the interactions: assert that the code called its collaborators correctly. Spies and mocks support this style; they observe or enforce the calls.

Neither is universally right. State verification asks "what came out?" and tends to survive refactoring, because it cares about outcomes, not mechanics. Behavior verification asks "what happened along the way?" and is the tool you need when the outcome is an interaction — an email that must be sent, a payment that must be charged, a cache that must be invalidated — with no return value to inspect.

Stub vs spy vs mock vs fake, side by side

FeatureStubSpyMockFake
PurposeFeed indirect inputRecord calls to assert laterEnforce expected callsReal logic, simplified
Returns canned values?✓✓✓computes real ones
Records calls?—✓✓—
Has expectations?——✓—
Verifiesstate (you assert)behavior (you assert after)behavior (self-verifies)state (you assert)
The four doubles you'll actually reach for, along the axes that distinguish them. (Dummy is omitted — it does nothing by definition.)

The names are blurred in real libraries

Meszaros's taxonomy is crisp; the tools are not. In practice, popular libraries collapse these categories. Jest gives you jest.fn() and jest.spyOn() that are really configurable spies with stub-like return control — you use the same object to stub returns and to assert calls. Sinon at least keeps three words — stub, spy, mock — but a Sinon stub can also record calls, which by Meszaros's definitions makes it a spy too. So do not expect the API name to map cleanly onto the concept. The useful move is to name the role the double plays in a given test — "here it's feeding input," "here it's verifying an interaction" — rather than trusting the method name. The taxonomy is a thinking tool, not an API contract.

When to use which — and the over-mocking trap

A workable default: prefer real objects; use stubs or fakes to supply inputs and verify state; reach for spies or mocks only when the behavior you care about is the interaction and there is no result to inspect. Save dummies for filling parameter lists you don't touch.

The trap is over-mocking. Every mock encodes an assumption about how your code talks to its collaborators, so a heavily mocked test is welded to the current implementation. Refactor the internals — split a method, reorder two calls, swap a collaborator — and a pile of tests go red even though the observable behavior never changed. Worse, the coupling cuts the other way: a suite of green, fully mocked unit tests tells you the pieces work in isolation against your assumptions, not that they work together. The real, un-mocked integration can be broken while every mock happily plays its scripted part.

Watch out

Heavily mocked tests fail in two directions: they break on harmless refactors (false alarms), and they stay green while the real integration is broken (missed bugs). Both erode trust in the suite. Mock at genuine boundaries — the network, the clock, third-party services — and use real objects or fakes within your own code.

This is exactly why the testing pyramid wants fewer, more integrated tests sitting above the mock-heavy unit layer: something has to exercise the wiring the mocks stubbed out. It is also why mock-driven design pairs naturally with test-driven development, where you use doubles to describe collaborations before the collaborators exist — but you still need higher-level tests to prove the collaborations are real. And it is a reminder that automated checks are one layer, not the whole story: see test automation vs manual testing for where each fits.

When the un-mocked path is what breaks

The unit suite is green. Every mock returned exactly what it was told to. And a user still hits a wall on the real checkout page, where the actual payment client, the real clock, and the live feature flags interact in a combination no mock ever rehearsed. Those escaped bugs — the ones that only surface once the doubles are gone — are precisely the ones a passing test can't explain. What closes them fast is a captured reproduction: the session replay, console error, and failing network request from the moment it broke, so the fix targets the real integration rather than another guess. That is the gap BugMojo fills — it records the un-mocked path your unit tests never saw.

Key takeaway

Test doubles are a vocabulary, not a hierarchy — no one of them is "best." A stub feeds input; a spy records calls to check later; a mock carries expectations and self-verifies; a fake runs real but simplified logic; a dummy just fills a slot. Pick by whether you're verifying state or behavior, and keep the mocking light enough that your tests still track the real system.

Catch the bug your mocks couldn't.

When a green test suite ships a broken integration, install the free BugMojo extension to capture a session replay plus console and network logs in one click — a reproduction the fix can actually target.

Install the extension

Frequently asked questions

Frequently asked questions

Sources

  1. Martin Fowler, 'Mocks Aren't Stubs' — the canonical essay defining state vs behavior verification and the test-double family — martinfowler.com (2026)
  2. Wikipedia — Test double: overview of dummy, stub, spy, mock, and fake — Wikipedia (2026)
  3. Martin Fowler, TestDouble — the umbrella term coined by Gerard Meszaros and the five categories — martinfowler.com (2026)
  4. ISTQB Glossary — formal definition of a test stub — ISTQB (2026)
Share:
Manvi
Manvi· QA Tester

Manvi is a Quality Assurance Tester with three years of experience. For her, quality is not just about finding bugs — it is about ensuring the best possible experience for every user.

On this page

  • What is a test double?
  • The five test doubles
  • Dummy — fills a slot, never used
  • Stub — canned answers (indirect input)
  • Spy — a stub that records how it was called
  • Mock — expectations that self-verify
  • Fake — a working, simplified implementation
  • The one split that matters: state vs behavior
  • Stub vs spy vs mock vs fake, side by side
  • The names are blurred in real libraries
  • When to use which — and the over-mocking trap
  • When the un-mocked path is what breaks

Get bug-tracking insights, weekly.

Engineering deep-dives, QA playbooks, and honest tool comparisons. No spam — unsubscribe in one click.