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

by Arif Ikhsanudin, Backend Developer

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

Manual Dependency Injection in Java — When It's Simpler Than Spring

Spring's dependency injection is powerful infrastructure for large applications. For smaller services, libraries, and tools, manual constructor injection with a composition root is often less code, faster to start, and easier to understand.

Read more

How to Laugh at Yourself After a Huge Mistake

We’ve all been there: the code breaks, the email goes to the wrong person, or the deployment wipes out production. Learning to laugh at these moments can save your sanity and even make you a better professional.

Read more

OAuth2 and JWT in Spring Boot — Resource Server Configuration, Token Validation, and Claims Extraction

A Spring Boot service that protects resources with OAuth2 JWT tokens is a resource server. Configuring one correctly requires understanding token validation, claims extraction, scope-based authorization, and how to test without a live authorization server.

Read more

How to Spot a Failing Software Project Before It Begins

“We haven’t even started yet… so why does this feel risky?” That gut feeling is often your first — and best — warning sign.

Read more