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.
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/specso mocks fail when called with methods or arguments that don't exist.
The code
# 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/autospecthat 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.