Python Mocking and Test Fixtures

Give tests fresh, controlled dependencies with fixtures, and replace slow or external services with mocks patched in the right place.

Lesson 61 of 107Advanced12 min read

Fixtures give each test the objects it needs, such as sample data, a temporary folder or a configured client, with set-up and clean-up handled for you. Mocks replace real dependencies, such as a weather API, a database or the clock, with stand-ins whose behaviour the test controls. Together they make tests fast, repeatable and independent of the outside world.

Python
import pytest
from unittest.mock import Mock

@pytest.fixture
def timetable():
    return {"East Pier": ["09:00", "11:00"]}

def test_departures(timetable):
    weather = Mock(return_value={"wind": 8})
    assert can_sail("East Pier", timetable, weather) is True

This lesson covers fixtures and their scopes, pytest's built-in fixtures, patching a dependency where it's looked up, and making mocks fail as well as succeed. The examples test a ferry timetable that cancels sailings in high wind. It builds on pytest in depth.

Fixtures

A fixture is a function decorated with @pytest.fixture. A test requests it by naming it as a parameter, and pytest calls the fixture and passes in the result:

Python
@pytest.fixture
def timetable():
    return {"East Pier": ["09:00", "11:00"], "West Quay": ["10:30"]}

def test_east_pier_has_two_sailings(timetable):
    assert len(timetable["East Pier"]) == 2

By default a fixture runs once per test, so each test gets a fresh dictionary and can modify it without affecting others. Fixtures can use other fixtures, and code after a yield runs as clean-up:

Python
@pytest.fixture
def log_file(tmp_path):
    path = tmp_path / "sailings.log"
    yield path
    # clean-up would go here; tmp_path is removed by pytest anyway

Shared fixtures go in a file called conftest.py, where pytest finds them automatically for every test in that folder and below.

Fixture scopes

scope= controls how often a fixture is created:

ScopeCreated once per
"function" (default)test
"class"test class
"module"test file
"session"whole test run

Wider scopes save time for expensive set-up, such as starting a test database or loading a large reference file. But a shared object is shared state: if one test changes a module-scoped dictionary, the next test sees the change, and tests start passing or failing depending on their order. Use wide scopes only for things that are read-only or expensive and safely reset between tests.

pytest provides useful built-in fixtures: tmp_path (a fresh temporary folder as a Path), monkeypatch (temporarily set attributes, environment variables and dictionary items, undone after the test), capsys (capture printed output) and caplog (capture log records).

Patching locations

unittest.mock.patch replaces a name with a mock for the duration of a test. The crucial rule: patch the name where the code under test looks it up, which is not necessarily where it was defined.

Python
# timetable.py
from weather import get_wind          # timetable now has its own name get_wind

def can_sail(pier):
    return get_wind(pier) < 30
Python
# test_timetable.py
from unittest.mock import patch
import timetable

def test_calm_day():
    with patch("timetable.get_wind", return_value=12):   # where it's used
        assert timetable.can_sail("East Pier")

Patching "weather.get_wind" here would have no effect, because from weather import get_wind copied the reference into timetable when it was imported; the code under test would still call the real function and make a real network request. If timetable.py instead did import weather and called weather.get_wind(...), you'd patch "weather.get_wind". monkeypatch.setattr(timetable, "get_wind", fake) follows the same rule.

Behavioral doubles

A test double only needs to reproduce the part of the dependency's contract the code relies on. Mock objects are configured with:

  • return_value: what calling the mock returns.
  • side_effect: an exception to raise, or a list of values to return on successive calls, or a function to call.
  • assertions such as assert_called_once_with(...) and call_count to check how the code used the dependency.
Python
from unittest.mock import Mock
get_wind = Mock(side_effect=TimeoutError("weather service timed out"))

Make doubles fail as well as succeed. A mock that always returns calm weather never exercises your timeout handling, which is exactly the code most likely to be broken. autospec=True (or create_autospec) makes a mock reject calls that don't match the real function's signature, so a test can't keep passing after the real API changes.

Don't over-mock: if a test mocks everything, it only proves the code calls its mocks in a particular order. Mock the slow and unpredictable boundaries, such as network, time, randomness and third-party services, and test real logic for real.

Independent practice and a worked check

Task: use fixtures for fresh timetable state and test successful and failed weather lookups with controlled doubles.

Python
import pytest
from unittest.mock import Mock

def next_sailing(timetable, pier, get_wind):
    if get_wind(pier) >= 30:
        return None
    return timetable[pier][0]

@pytest.fixture
def timetable():
    return {"East Pier": ["09:00", "11:00"]}

def test_calm_returns_first_sailing(timetable):
    assert next_sailing(timetable, "East Pier", Mock(return_value=10)) == "09:00"

def test_storm_cancels(timetable):
    assert next_sailing(timetable, "East Pier", Mock(return_value=35)) is None

def test_timeout_propagates(timetable):
    with pytest.raises(TimeoutError):
        next_sailing(timetable, "East Pier", Mock(side_effect=TimeoutError))

All three tests pass, and each receives its own timetable. For the worked check, a dependency should usually be patched where the tested code looks it up, the module namespace that holds the name the code calls.

Reference and next practice

The documentation covers unittest.mock, including the section where to patch, and pytest's fixtures guide. Continue with PEP 8 style and revisit web APIs. Practise with the Python exercises and Python quiz.

Mocking and Test Fixtures Summary

  • Fixtures: Fixtures supply reusable test dependencies with a managed lifetime.
  • Fixture scopes: Function, module and session scopes control how long fixture instances are reused.
  • Patching locations: Patch the name used by the code under test rather than guessing its original definition location.
  • Behavioral doubles: A fake or mock should reproduce the part of a dependency contract the test relies on.

Mocking and Test Fixtures Examples

Each example shows the code, the output it produces and what each part does.

Example 1

Mocking and Test Fixtures in a small program

Python
from unittest.mock import Mock
weather = Mock(return_value={"wind": 8})
report = weather("East Pier")
print(report["wind"])
weather.assert_called_once_with("East Pier")
Output
8
Line-by-line explanation
  1. Line 2A Mock configured to return a fixed weather report when called.
  2. Line 3Calling the mock records the call and returns the dictionary.
  3. Line 5The assertion checks the code under test asked for the right pier; it raises AssertionError otherwise.
Example 2

Fixtures and doubles for success and failure

Python
from unittest.mock import Mock

def sailing_status(pier, get_wind):
    try:
        wind = get_wind(pier)
    except TimeoutError:
        return "unknown: weather service unavailable"
    return "cancelled" if wind >= 30 else "on time"

calm = Mock(return_value=12)
storm = Mock(return_value=41)
offline = Mock(side_effect=TimeoutError)

for name, double in [("calm", calm), ("storm", storm), ("offline", offline)]:
    print(f"{name:<8}{sailing_status('East Pier', double)}")

calm.assert_called_once_with("East Pier")
print("calls:", calm.call_count, storm.call_count, offline.call_count)
Output
calm    on time
storm   cancelled
offline unknown: weather service unavailable
calls: 1 1 1
Line-by-line explanation
  1. Line 3Passing get_wind in as a parameter makes the dependency easy to replace, with no patching needed.
  2. Line 12side_effect=TimeoutError makes the double raise, exercising the error branch.
  3. Line 17The mock records how it was called, so the test can check the right pier was requested.

Common Mocking and Test Fixtures Mistakes

Patching where the function is defined instead of where it's used

Don't
# timetable.py did: from weather import get_wind
with patch("weather.get_wind", return_value=12):
    timetable.can_sail("East Pier")      # still calls the real get_wind
Do
with patch("timetable.get_wind", return_value=12):
    timetable.can_sail("East Pier")

Sharing a mutable fixture across tests

Don't
@pytest.fixture(scope="module")
def timetable():
    return {"East Pier": ["09:00"]}      # one dict for the whole file

def test_add(timetable):
    timetable["East Pier"].append("17:00")   # leaks into later tests
Do
@pytest.fixture                          # fresh dict for every test
def timetable():
    return {"East Pier": ["09:00"]}

Mocking and Test Fixtures in Real Projects

Fixtures and mocks in real test suites

Web applications use fixtures for a test client, a database rolled back after every test, and logged-in users with different permissions. External HTTP calls are replaced with mocks or with libraries such as responses and respx that intercept requests. Time-dependent code is tested by freezing the clock, randomness by seeding or mocking random, and file handling with tmp_path.

Many teams design code so dependencies are passed in, as in the example above, which makes tests simpler and reduces the need for patch at all. When patching is necessary, the "where it's looked up" rule prevents the classic surprise of a test that quietly calls a real service.

Mocking and Test Fixtures FAQ

What is a fixture in pytest?

A function marked @pytest.fixture that prepares something a test needs. Tests receive it by naming it as a parameter. Fixtures can depend on other fixtures, clean up after a yield, and be shared through conftest.py.

What is the difference between Mock and MagicMock?

MagicMock is a Mock that also supports Python's magic methods, such as len(), iteration and context-manager use. patch() creates a MagicMock by default. Use plain Mock when you want unsupported operations to fail loudly.

Why is my patch not working?

Usually because the name was patched in the module where it's defined rather than where it's used. If the code does from weather import get_wind, patch "yourmodule.get_wind". Also make sure the patch is active while the code under test runs.

Should I use monkeypatch or unittest.mock.patch?

Both work in pytest. monkeypatch is a fixture that's convenient for setting attributes, environment variables and dictionary items; patch creates mock objects with call recording and assertions. Many tests use monkeypatch.setattr with a Mock to get both.

How do I make a mock raise an exception?

Set side_effect to an exception class or instance: Mock(side_effect=TimeoutError("slow")). Set it to a list to return different values on successive calls, where list items that are exceptions are raised.

Mocking and Test Fixtures Exercises

Answer each one and check it instantly. If you get stuck, open the hint before the answer.

Multiple choiceeasy

Where should a dependency usually be patched?

Multiple choicemedium

Which review concern is valid for mocking and test fixtures?

Mocking and Test Fixtures Quiz

2 questions to check what you learned.

  1. Where should a dependency usually be patched?

    • Only in its original module
    • Where the tested code looks it up
    • Python ignores the distinction
  2. Which statement correctly describes patching locations?

    • Patch the name used by the code under test rather than guessing its original definition location.
    • Only the example output matters, not the rule
    • Every operation has the same behavior