02 — Dependency Inversion, Fakes vs Mocks & Boundary Testing
At a glance
After completing this lesson you can:
- Apply the Dependency Inversion Principle (DIP) to decouple domain logic from external I/O (databases, network APIs, disk).
- Contrast Mocks (verifying interactions/calls) with Fakes (in-memory functional implementations) and explain why over-mocking creates fragile tests.
- Build clean, stateful in-memory fake repositories for rapid testing without spinning up Docker or external services.
Time: ~50 minutes · Prerequisites: 01 — Deterministic Testing
Reading Companion
- Book: Architecture Patterns with Python (Cosmic Python) — Harry Percival & Bob Gregory
- Chapter 2: Repository Pattern (Building a Fake Repository that behaves like the real database).
Why this matters in production
Teams that rely heavily on unittest.mock.patch often discover that their tests pass with 100% code coverage while production completely crashes. Why? Because mocks test implementation details rather than behavior. When you change a method name or database driver, all mocks must be rewritten. In-memory Fakes implement real business rules, providing reliable verification in microseconds.
Deep dive
1. Dummy vs Stub vs Spy vs Mock vs Fake (Gerard Meszaros Taxonomy)
- Stub: Returns canned answers (
lambda: 200). - Mock: Expects specific method calls with specific arguments (
assert_called_once_with(...)). - Fake: Working, simplified implementation (e.g. an in-memory dictionary acting as a database).
Hands on
python3 phase0-foundations/04-testing-and-verification/02-fakes-mocks-and-boundaries/examples/01_demo.py
python3 phase0-foundations/04-testing-and-verification/02-fakes-mocks-and-boundaries/examples/02_trap.py
AI Trap & Code Review
# BAD AI-GENERATED CODE — BRITTLE OVER-MOCKING
@patch("services.orders.requests.post")
@patch("services.orders.time.time")
@patch("services.orders.db.session.query")
def test_create_order(mock_query, mock_time, mock_post):
# Tests that internal libraries were called in a specific order, NOT business correctness!
Exercises
- Rung 1 (
UserRepository Protocol): Define aUserRepositoryprotocol specifyingget_by_id(user_id: int) -> dict | Noneandsave(user: dict) -> None. - Rung 2 (
FakeUserRepository— Fix the AI Bug): Implement a dictionary-backed in-memory fake adhering toUserRepositorythat deep-copies saved objects to prevent in-place mutation leaks. - Rung 3 (
FakeNotificationSender): Implement an in-memory notification sender recording sent messages in a listsent: list[tuple[str, str]]with a.has_sent_to(recipient: str) -> boolhelper. - Rung 4 (
UserRegistrationService): Implement business service that registers users, saves to the repository, and dispatches a welcome notification via injected dependencies. - Rung 5 (
FakeClock & Expiry Service): Build a fake clock and test an account deactivation service across time boundaries withoutsleep.
Wrapping up
Can you explain?
- Why does over-using
unittest.mock.patchmake refactoring difficult? - What is the key advantage of a Fake Repository over a Mock?
- How does Dependency Inversion protect domain logic from database changes?
Further Reading & References
- Martin Fowler: Mocks Aren't Stubs
- Google Testing Blog: Testing on the Toilet - Know Your Test Doubles
- Architecture Patterns with Python (Cosmic Python): Chapter 3 - A Brief Interlude
- pytest Documentation: Fixture Scope and Dependency Injection
Show reference solution notes
Solution & Engineering Notes — 02 Dependency Inversion & Fakes
- Isolation in Fakes: Always
copy.deepcopy()data when saving or retrieving from in-memory Fakes. If the calling code mutates an object after passing it tosave(), real databases remain isolated, but a naive Fake dict will mutate in-place, creating false test passes. - Decoupling business logic: High-level policy (user registration) depends only on abstract interfaces (
UserRepository), not low-level database drivers (SQLAlchemy, psycopg).
Self-check
Run python3 -m unittest phase0-foundations/04-testing-and-verification/02-fakes-mocks-and-boundaries/test_exercises.py -v until green, then record it here. Saved to progress/rungs.json; commit it.
Completion edits PROGRESS.md; commit it with the lesson.