Python: inspect interleaved unittest.mock calls with attached mocks

When you use a unittest.mock mock, you often make assertions on the calls it received, such as through the mock_calls list. But sometimes you want to assert on the order of calls across multiple mocked functions, for example to check that steps in a process happen in the right sequence.
To do this, use attached mocks, which let you collect calls from multiple mocks into a single timeline. A parent mock has multiple child mocks attached to it, and its mock_calls list records calls from all of its children together, interleaved in the order they actually happened. Sometimes that attachment happens automatically, and sometimes you have to set it up yourself.
Say we have a Kettle class, and a function that fills one by opening the lid, adding water, and closing the lid again:
class Kettle:
def open(self) -> None: ...
def add_water(self, litres: float) -> None: ...
def close(self) -> None: ...
def fill(kettle: Kettle, litres: float) -> None:
kettle.open()
kettle.add_water(litres)
kettle.close()
To test that fill() calls those three methods in order, pass in a mock Kettle:
from unittest import TestCase, mock
from example import Kettle, fill
class FillTests(TestCase):
def test_fill_order(self):
kettle = mock.create_autospec(Kettle, instance=True)
fill(kettle, 1.5)
assert kettle.mock_calls == [
mock.call.open(),
mock.call.add_water(1.5),
mock.call.close(),
]
mock.create_autospec() builds a mock that matches Kettle’s shape, checking arguments against the real method signatures. instance=True makes it stand in for an instance of the class, rather than the class itself.
The method mocks, like open(), are created on access and automatically attached to the parent mock, kettle. That’s how kettle.mock_calls contains all three calls in order, even though we never explicitly attached them, and a single assertion can check the whole sequence.
This all works the same if you use spies rather than plain mocks, by adding the wraps argument, as covered in my previous post. That often makes a better test, since the test would then exercise the real behaviour of the wrapped object as well as recording the calls.
Separate functions need attach_mock()
Automatic attachment only helps when the calls you care about are already methods on one object. Say fill_kettle() instead calls three separate module-level functions:
def open_lid() -> None: ...
def fill_water(litres: float) -> None: ...
def close_lid() -> None: ...
def fill_kettle(litres: float) -> None:
open_lid()
fill_water(litres)
close_lid()
open_lid, fill_water, and close_lid are entirely separate functions, so mocking them separately gives you three separate mocks, each with its own independent mock_calls, and no shared parent to collect them together. This is where Mock.attach_mock() comes in: use it to attach each mock to a bare parent mock.
from unittest import TestCase, mock
import example
from example import fill_kettle
class FillKettleTests(TestCase):
def test_fill_kettle_order(self):
with (
mock.patch.object(example, "open_lid", autospec=True) as open_lid_mock,
mock.patch.object(example, "fill_water", autospec=True) as fill_water_mock,
mock.patch.object(example, "close_lid", autospec=True) as close_lid_mock,
):
parent = mock.Mock()
parent.attach_mock(open_lid_mock, "open_lid")
parent.attach_mock(fill_water_mock, "fill_water")
parent.attach_mock(close_lid_mock, "close_lid")
fill_kettle(1.5)
assert parent.mock_calls == [
mock.call.open_lid(),
mock.call.fill_water(1.5),
mock.call.close_lid(),
]
parent doesn’t mock anything itself, and so is never called. It exists only to collect calls from its attached children. Once attached, calls to any of the three mocks also appear on parent.mock_calls, prefixed with the name each was attached under, interleaved in the order they actually happened.
😸😸😸 Check out my new book on using GitHub effectively, Boost Your GitHub DX! 😸😸😸
One summary email a week, no spam, I pinky promise.
Related posts: