02 — Dependency Inversion, Fakes vs Mocks & Boundary Testing

At a glance

After completing this lesson you can:

Time: ~50 minutes · Prerequisites: 01 — Deterministic Testing

Reading Companion

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)

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

  1. Rung 1 (UserRepository Protocol): Define a UserRepository protocol specifying get_by_id(user_id: int) -> dict | None and save(user: dict) -> None.
  2. Rung 2 (FakeUserRepository — Fix the AI Bug): Implement a dictionary-backed in-memory fake adhering to UserRepository that deep-copies saved objects to prevent in-place mutation leaks.
  3. Rung 3 (FakeNotificationSender): Implement an in-memory notification sender recording sent messages in a list sent: list[tuple[str, str]] with a .has_sent_to(recipient: str) -> bool helper.
  4. Rung 4 (UserRegistrationService): Implement business service that registers users, saves to the repository, and dispatches a welcome notification via injected dependencies.
  5. Rung 5 (FakeClock & Expiry Service): Build a fake clock and test an account deactivation service across time boundaries without sleep.

Wrapping up

Can you explain?

  1. Why does over-using unittest.mock.patch make refactoring difficult?
  2. What is the key advantage of a Fake Repository over a Mock?
  3. How does Dependency Inversion protect domain logic from database changes?

Further Reading & References

Show reference solution notes

Solution & Engineering Notes — 02 Dependency Inversion & Fakes

  1. 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 to save(), real databases remain isolated, but a naive Fake dict will mutate in-place, creating false test passes.
  2. 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.

Confidence:

Completion edits PROGRESS.md; commit it with the lesson.