The Difference Between a Mock, a Stub, and a Fake That Actually Matters

by Eric Hanson, Backend Developer at Clean Systems Consulting

Why the Distinction Matters

Gerard Meszaros coined the term "test double" in xUnit Test Patterns to cover the full category of objects used in tests to replace real dependencies. Mock, stub, fake, spy, and dummy are all test doubles — but they play different roles and have different implications for test design.

Using the wrong one does not usually break the test, but it often makes the test harder to read, more brittle, or less useful. The distinction matters most when you are deciding what to verify.

Stub: Control Indirect Inputs

A stub provides predetermined responses to calls made during the test. It controls the indirect inputs to the code under test. The test does not assert on the stub — it uses the stub to put the code into a specific state.

# Stub: controls what the code receives from a dependency
def test_applies_correct_tax_for_california():
    tax_stub = Mock()
    tax_stub.get_rate.return_value = 0.085  # CA rate

    calculator = PriceCalculator(tax_service=tax_stub)
    result = calculator.total(price=100.0, region="CA")

    assert result == 108.50  # The assertion is on the output, not the stub

The stub's job is to make get_rate return a value you control. The test asserts on the output of PriceCalculator, not on what tax_stub was called with. The stub enables the test condition; it is not what is being tested.

Mock: Verify Indirect Outputs

A mock is used to verify that the code under test made specific calls to a dependency. The assertion is on the mock itself. This is appropriate when the behavior you are testing is the side effect — the call that was made — rather than a return value.

# Mock: verifies that a call was made (the side effect IS the behavior)
def test_sends_confirmation_email_after_successful_order():
    email_mock = Mock()
    order_service = OrderService(email_service=email_mock)

    order_service.place_order(customer_id=1, items=[item_a])

    email_mock.send_confirmation.assert_called_once_with(
        to="customer@example.com",
        order_id=ANY
    )

This is a legitimate use of a mock: the observable behavior of place_order includes sending a confirmation email. The email is a side effect with no return value to assert on, so verifying the call is the right approach.

The risk with mocks: overusing them for internal implementation details rather than observable side effects. If you are mocking calls that are internal wiring — one method calling another within a service — those mocks will break every time you refactor, even when the behavior is correct.

Fake: A Working Implementation

A fake is a real implementation that is simplified for test use. It does not use predetermined responses — it implements actual logic, just without the infrastructure overhead.

// Fake: real implementation, no I/O
type InMemoryUserRepository struct {
    users map[int64]*User
    nextID int64
}

func (r *InMemoryUserRepository) Save(user *User) (*User, error) {
    r.nextID++
    user.ID = r.nextID
    r.users[user.ID] = user
    return user, nil
}

func (r *InMemoryUserRepository) FindByEmail(email string) (*User, error) {
    for _, u := range r.users {
        if u.Email == email {
            return u, nil
        }
    }
    return nil, ErrNotFound
}

Fakes are more work to create but produce more robust tests. Tests using a fake verify real behavior — the FindByEmail logic actually runs, and a test that calls Save followed by FindByEmail exercises the contract between them. Tests using mocks verify interaction patterns, which are fragile under refactoring.

Use fakes for dependencies you own and will maintain. Use stubs and mocks for dependencies you do not control (external APIs, third-party libraries) or for dependencies with expensive infrastructure requirements.

Spy: Record and Verify Selectively

A spy is a real object that records calls made to it so they can be verified afterward. It differs from a mock in that it delegates to the real implementation — it is a wrapper, not a replacement.

// Spy wraps a real object, records interactions
NotificationService realService = new NotificationService(smtpConfig);
NotificationService spy = Mockito.spy(realService);

orderService.processOrder(order);

// Verify call was made, but real implementation also ran
verify(spy).sendConfirmation(order.getCustomerId());

Spies are useful when you want to verify a call was made but also want the real implementation to execute — for integration-style tests where you need both the side effect and the verification.

The Quick Decision Guide

  • Stub when you need to control what a dependency returns to your code
  • Mock when the test is about verifying a side effect (a call that was made)
  • Fake when you own the dependency and want tests that survive refactoring
  • Spy when you need both real execution and interaction verification

When in doubt: prefer fakes for dependencies you own, stubs for dependencies you do not, and mocks only when the side effect is what you are actually testing.

Scale Your Backend - Need an Experienced Backend Developer?

We provide backend engineers who join your team as contractors to help build, improve, and scale your backend systems.

We focus on clean backend design, clear documentation, and systems that remain reliable as products grow. Our goal is to strengthen your team and deliver backend systems that are easy to operate and maintain.

We work from our own development environments and support teams across US, EU, and APAC timezones. Our workflow emphasizes documentation and asynchronous collaboration to keep development efficient and focused.

  • Production Backend Experience. Experience building and maintaining backend systems, APIs, and databases used in production.
  • Scalable Architecture. Design backend systems that stay reliable as your product and traffic grow.
  • Contractor Friendly. Flexible engagement for short projects, long-term support, or extra help during releases.
  • Focus on Backend Reliability. Improve API performance, database stability, and overall backend reliability.
  • Documentation-Driven Development. Development guided by clear documentation so teams stay aligned and work efficiently.
  • Domain-Driven Design. Design backend systems around real business processes and product needs.

Tell us about your project

Our offices

  • Copenhagen
    1 Carlsberg Gate
    1260, København, Denmark
  • Magelang
    12 Jalan Bligo
    56485, Magelang, Indonesia

More articles

Why Paris Startups Are Quietly Routing Backend Work to Async Remote Contractors

Nobody talks about it at Station F happy hours. But the startups shipping fastest have stopped hiring for every backend project on their roadmap.

Read more

How to Say “No” to Unreasonable Requests Professionally

Learning to say “no” is one of the hardest skills for developers and managers alike. Here’s how to protect your time without burning bridges.

Read more

The Pull Request That Was Too Big to Review Properly

Large pull requests are one of the most reliable predictors of poor code review quality. The cognitive overhead of reviewing a 2,000-line change is high enough that reviewers inevitably skim — and the bugs they miss ship.

Read more

From Outsider to ‘Employee’: The Danger of Over-Controlled Contractors

“Just follow our internal process and be online during office hours.” That’s usually how a contractor slowly stops feeling like a contractor.

Read more