Fakes, stubs and mocks

Testing backends Junior Python 3.13+ PostgreSQL 17+ Reviewed By Best Latest Apps

Predict the output

What do the two lines print?

Python 3.13
from typing import Protocol


class Users(Protocol):
    def get_email(self, user_id: int) -> str | None: ...


class FakeUsers:
    def __init__(self, data: dict[int, str]) -> None:
        self.data = data

    def get_email(self, user_id: int) -> str | None:
        return self.data.get(user_id)


def greeting(users: Users, user_id: int) -> str:
    email = users.get_email(user_id)
    return f"Hi {email}" if email else "Hi guest"


fake = FakeUsers({1: "ana@example.com"})
print(greeting(fake, 1))
print(greeting(fake, 2))

Options
Hint 1 of 1

What does dict.get return for a missing key?

Choose an answer, then open Show answer below to check it.

Show answer

Answer: A Hi ana@example.com then Hi guest

Correct. The fake knows user 1 and returns None for 2.

A fake is a small working implementation, here a dict behind the same method. It returns data for user 1 and None for user 2, so the test checks real behaviour, not which calls were made.

Why not the others?
  • B Hi ana@example.com twice. User 2 isn't in the fake's data.
  • C A type error. FakeUsers matches the Users protocol structurally.
  • D Hi None then Hi guest. User 1 exists.

30-second answer

A stub returns canned answers. A fake is a working lightweight implementation, such as an in-memory repository behind the same interface. A mock records calls so the test can assert how it was used. Prefer fakes for collaborators you own: tests check outcomes and survive refactoring. Use mocks sparingly, for verifying a side effect you can't observe otherwise, like 'an email was sent once'.

Practise saying it · 30 s. No recording, just a timer

Fakes vs mocks.

Say your answer out loud, then check which of these points you covered:

  • Stub: canned
  • Fake: working in-memory
  • Mock: verify calls
  • Prefer fakes
  • Shared contract tests

Deep dive

Define the dependency as a Protocol so fakes are type-checked.

Over-mocking couples tests to implementation details.

Don't mock what you don't own; wrap third-party clients in an adapter and fake the adapter.

Run the same contract tests against the fake and the real implementation.

A typed in-memory fake Ran on Python 3.13.5 · mypy --strict
Python 3.13
from typing import Protocol


class Users(Protocol):
    def get_email(self, user_id: int) -> str | None: ...


class FakeUsers:
    def __init__(self, data: dict[int, str]) -> None:
        self.data = data

    def get_email(self, user_id: int) -> str | None:
        return self.data.get(user_id)


def greeting(users: Users, user_id: int) -> str:
    email = users.get_email(user_id)
    return f"Hi {email}" if email else "Hi guest"


fake = FakeUsers({1: "ana@example.com"})
print(greeting(fake, 1))
print(greeting(fake, 2))

Common mistakes

  • Asserting every internal call.
  • Mocking types from third-party libraries directly.
  • Fakes that drift from the real behaviour.

Senior take & follow-ups

Define the three, prefer fakes for owned code, mocks for side effects, keep fakes honest.

Follow-up questions

Think of your answer first, then open the hint.

  1. How do you keep a fake honest?

    Show hint

    Run one shared test suite against both fake and real.

  2. When is a mock right?

    Show hint

    When the call itself is the behaviour, like sending a message.

Similar on Android: Testing a ViewModel The Android page fakes a repository under a ViewModel; here a fake stands in for a backend dependency.

Suggest a fix if something here is wrong or out of date.