Testing in Practice · Foundations: Strategy, pytest and Test Doubles

Test Doubles: Fakes, Stubs and Mocks Without Over-Mocking

Replace slow or external dependencies with fakes and mocks at system boundaries, and avoid tests that only check your mocks.

Create a free account to save this page, take notes and track your progress. Sign in

What you'll build

Tests for a sign-up service that saves users and sends a welcome email. The repository is replaced by an in-memory fake, the email provider by a mock that records calls, and you'll see an over-mocked test that passes while the code is broken.

The idea

  • Stub: returns canned answers. Fake: a working lightweight implementation (in-memory repository). Mock: records calls so you can assert on them.
  • Replace dependencies at boundaries you don't own or that are slow: email, payment, HTTP APIs, clocks. Keep your own logic real.
  • Inject dependencies (constructor parameters) so tests can pass doubles without patching module internals.
  • Use autospec/spec so mocks fail when called with methods or arguments that don't exist.

The code

A fake repository, a mock email sender, and an over-mocked test
# file: signup.py
class SignupService:
    def __init__(self, users, mailer):
        self.users = users
        self.mailer = mailer

    def sign_up(self, email: str) -> str:
        email = email.strip().lower()
        if self.users.exists(email):
            raise ValueError("email already registered")
        self.users.add(email)
        self.mailer.send(to=email, template="welcome")
        return email

# file: test_signup.py
from unittest.mock import create_autospec
import pytest
from signup import SignupService

class FakeUsers:                                   # a fake: a real, tiny implementation
    def __init__(self):
        self.emails = set()
    def exists(self, email):
        return email in self.emails
    def add(self, email):
        self.emails.add(email)

class Mailer:                                      # the real interface, used for autospec
    def send(self, to: str, template: str) -> None: ...

def test_new_user_is_saved_and_welcomed():
    users, mailer = FakeUsers(), create_autospec(Mailer, instance=True)
    SignupService(users, mailer).sign_up("  [email protected] ")
    assert users.exists("[email protected]")
    mailer.send.assert_called_once_with(to="[email protected]", template="welcome")

def test_duplicate_email_is_rejected_and_no_mail_sent():
    users, mailer = FakeUsers(), create_autospec(Mailer, instance=True)
    service = SignupService(users, mailer)
    service.sign_up("[email protected]")
    with pytest.raises(ValueError):
        service.sign_up("[email protected]")
    assert mailer.send.call_count == 1

def test_over_mocked_passes_whatever_the_code_does(monkeypatch):
    from unittest.mock import Mock
    service = SignupService(Mock(), Mock())
    monkeypatch.setattr(service, "sign_up", Mock(return_value="[email protected]"))   # mocks the code under test!
    assert service.sign_up("[email protected]") == "[email protected]"

def test_autospec_catches_wrong_calls():
    mailer = create_autospec(Mailer, instance=True)
    with pytest.raises(TypeError):
        mailer.send("[email protected]")              # missing the template argument

# file: main.py
import pytest
pytest.main(["-q", "--tb=short", "-p", "no:cacheprovider"])

Output

....                                                                     [100%]
4 passed in <time>

Run this on your computer — see “Get Started” for setup.

The third test passes without running any of the service's code: it replaced the method under test with a mock. Tests that assert on their own mocks prove nothing; keep the code under test real.

Common mistakes

  • Mocking your own classes everywhere, so tests only verify wiring and break on refactors.
  • patch("module.thing") on the wrong import path, leaving the real dependency in place.
  • Mocks without spec/autospec that accept any method name or argument, hiding typos.
  • Faking the database for tests about queries; use a real database for those (next lesson).

Key points

  • Double external boundaries; keep your own logic real.
  • Prefer injected fakes; use autospec mocks to verify interactions.
  • A test that only exercises mocks proves nothing.

Further reading: unittest.mock · Autospeccing · Martin Fowler: mocks aren't stubs

Practice questions

What's the difference between a fake and a mock?

Answer

A fake is a working lightweight implementation (such as an in-memory repository); a mock records calls so the test can assert on interactions.

What does autospec add to a mock?

Answer

The mock copies the real object's signatures, so calls with missing or wrong arguments, or unknown methods, fail.

Which dependencies should you replace with doubles?

Answer

Slow or external boundaries you don't control (email, payments, third-party APIs, clocks), not your own business logic.

Open in the app to mark lessons complete, take notes and track your progress.

Was this page helpful?