Skip to content
DevPedia

Unit testingGuide 5 of 13

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

What role each kind of double plays and how to pick the right one for controlling a test's dependencies.

Updated 6 min read

// on this page

A test double is a stand-in used during testing to represent a real dependency. The most common terms — dummy, stub, spy, mock, fake — aren’t interchangeable: each one plays a different role inside the test, and that distinction matters more than the tool used to create them.

The taxonomy comes from Gerard Meszaros’s xUnit Test Patterns. It’s best not to read it as a scale from less to more, because the five don’t line up along a single axis: there are two independent ones.

Not part of the verificationPart of the verification
No behavior of its ownDummy—
Returns fixed responsesStubMock
Records what happened to itSpySpy + assertions
Working implementationFake—

The vertical axis is how much real implementation it has. The horizontal axis is whether the double is part of what the test verifies. A mock has no more real behavior than a stub: it’s a stub that additionally demands to have been called in a certain way.

That distinction translates into a practical difference that does matter:

  • State verification — the test looks at the result or the final state of the system. Use a dummy, a stub, or a fake.
  • Interaction verification — the test looks at how the unit used its collaborator. Use a spy or a mock.

Dummy

Used because a dependency has to be present, but the test doesn’t interact with it:

const logger = new DummyLogger();
const service = new ReservationService(reservationRepository, logger);

Stub

Returns predefined responses. If ReservationService asks PricingService how much deposit to charge, the test controls that response and then verifies the result:

const pricingStub: PricingService = {
  getDepositAmount: () => Money.of(2000),
};

const service = new ReservationService(pricingStub, tableRepository);
const reservation = service.create(request);

// The assertion is about the result, not about the stub
expect(reservation.deposit).toEqual(Money.of(2000));

The question behind a stub is: what response do I need this dependency to hand back so I reach the case I want to verify? The focus isn’t on how it was used, it’s on the final state.

Spy

Records what happened to it, so the test can inspect it after the action. If ReservationService notifies NotificationService when a reservation is confirmed, the spy notes the call and the test reviews it at the end:

class NotificationSpy implements NotificationService {
  readonly sent: ReservationId[] = [];

  sendConfirmation(id: ReservationId): void {
    this.sent.push(id);   // only records, doesn't judge
  }
}

// Act
const spy = new NotificationSpy();
new ReservationService(pricingStub, spy).confirm(reservationId);

// Assert: the verification lives in the test, at the end
expect(spy.sent).toEqual([reservationId]);

The spy is passive: it doesn’t know what’s expected of it. All the judgment is in the assertions block.

Mock

A mock carries the expectation inside: you tell it in advance which calls it should receive, and it fails on its own if they don’t happen as agreed.

const paymentMock = mock<PaymentGateway>();
// The expectation is declared BEFORE running
paymentMock.expect('charge').once().with(Money.of(2000));

new ReservationService(pricingStub, paymentMock).confirm(reservationId);

// There's no assertion on values: the mock is asked to verify itself
paymentMock.verify();

Mocks are justified when the interaction is the behavior: that the charge happens exactly once, that the event is published, that the provider isn’t called if the reservation was already cancelled. For everything else, verifying the result usually stays valid when the implementation changes.

Fake

A simplified but working implementation of a dependency. Unlike a stub, a fake has behavior of its own:

const repository = new InMemoryReservationRepository();

repository.save(reservation);

const result = repository.findById(reservation.id);

An InMemoryReservationRepository can be far simpler than Postgres and still be a realistic enough representation for certain kinds of test.

Test doubleMain purpose
DummySatisfy a dependency that isn’t used
StubHand back controlled responses
SpyRecord interactions
MockVerify expected interactions, failing on its own
FakeA simplified but working implementation

The rule that sums the article up: pick the double based on what the test needs to verify, not on the dependency you’re replacing. The same PaymentService can be a dummy in a validation test, a stub in a calculation test, and a mock in one that guards against charging twice.

It’s no accident that replacing a dependency is easy: it depends on the unit receiving an abstraction instead of constructing a concrete implementation directly. When isolating a dependency turns out to be hard, that’s usually a sign the design has unnecessary coupling. It’s one of the central ideas of dependency inversion.

Isolation, AAA, and test doubles together let you build unit tests that are clear and maintainable. Test-Driven Development builds on these same ideas: using tests as part of the design process, not only as after-the-fact verification.

Share this guide

Search by concept, pattern or practice.