Python Pytest Mocker: Patch, Spy, and Call Assertions
python pytest mocker patch spy and call assertions: How to use pytest-mock's mocker fixture for patching, spying, and call assertions in Python unit tests, including w...
Unit tests that touch external services, databases, or timing-sensitive code usually need to replace those dependencies with mocks. In pytest, the pytest-mock plugin provides the mocker fixture, the standard way to handle python pytest mocker patch spy and call assertions without manual cleanup. The fixture replaces dependencies, records calls, and restores the original objects automatically when each test ends.
The mocker fixture and why it exists
mocker is an instance-based wrapper around unittest.mock. Every test receives a fresh instance, and pytest-mock stops every patch and spy created through it at the end of the test. That removes the need for try/finally blocks, context managers, or tearDown methods that restore the original objects.
Because mocker is a regular fixture, it composes with parametrized tests and other fixtures. Any test or fixture that needs to replace a dependency can request it directly.
mocker.patch: replacing dependencies
mocker.patch replaces an attribute on a module or class with a MagicMock. The most common form is patching by string target:
from myapp.orders import send_confirmation_email def test_send_confirmation_email(mocker): mock_send = mocker.patch("myapp.orders.send") send_confirmation_email("user@example.com", order_id=1234) mock_send.assert_called_once_with("user@example.com", order_id=1234)
The string target is the full dotted path to the attribute. The mock replaces the attribute for the duration of the test, and the original is restored automatically.
You can control what the mock returns or does:
def test_order_total_uses_discount(mocker): mock_apply = mocker.patch("myapp.orders.apply_discount", return_value=90.0) total = calculate_total(100, 0.1) assert total == 90.0 mock_apply.assert_called_once_with(100, 0.1)
For behavior like raising an exception, use side_effect:
def test_retry_on_timeout(mocker): mocker.patch("myapp.orders.fetch_stock", side_effect=TimeoutError) # code under test retries and eventually succeeds
mocker.patch also provides mocker.patch.object(Class, "method") for patching a single method, and mocker.patch.multiple for replacing several attributes at once.
mocker.spy: observing without replacing
mocker.spy wraps an existing function or method so the original logic still runs, but every call is recorded. This is useful when you want to verify that code took a particular path without changing its behavior.
from myapp import pricing def test_apply_discount_records_call(mocker): spy = mocker.spy(pricing, "apply_discount") result = pricing.apply_discount(100, 0.1) assert result == 90 spy.assert_called_once_with(100, 0.1)
The spy object supports the same call assertions as a mock, but the underlying function executes normally. If the original function has side effects, they still happen.
A spy is appropriate when the real implementation is fast, deterministic, and safe to run in a test. For external I/O or non-deterministic behavior, patch is usually the better choice.
Call assertions and the call object
Both mocks and spies expose the standard call assertions from unittest.mock:
assert_called()assert_called_once()assert_called_with(*args, **kwargs)assert_called_once_with(*args, **kwargs)assert_has_calls(calls, any_order=False)assert_not_called()
When you need to verify a sequence of calls, use the call object:
from unittest.mock import call def test_retry_sequence(mocker): mock_fetch = mocker.patch("myapp.orders.fetch_stock") fetch_stock_with_retry("SKU-100") mock_fetch.assert_has_calls([ call("SKU-100"), call("SKU-100"), ])
By default assert_has_calls checks that the calls appear in the given order. Pass any_order=True when order does not matter.
A common mistake is using assert_called_once_with when the function is invoked more than once. The assertion fails with a message listing the actual calls, which usually points to the real bug: either the code calls the function twice, or the test is asserting on the wrong mock.
Choosing between patch and spy
The decision comes down to whether the real implementation should run.
Use mocker.patch when:
- The dependency performs I/O, network access, or other side effects.
- You need to control the return value to test different branches.
- The real implementation is slow or non-deterministic.
Use mocker.spy when:
- The implementation is safe and fast to run.
- You only need to confirm that it was called with certain arguments.
- You want the real return value to flow through the code under test.
A spy is not a substitute for patch when the goal is to isolate the unit from its dependencies. If the real function hits a database, a spy will still hit the database.
Common pitfalls when patching and asserting
Patching the wrong target
If a module imports a name at the top level, patching the defining module does not affect the already-bound reference:
# myapp/orders.py from myapp.mailer import send # This does not replace the send used inside orders.py: mocker.patch("myapp.mailer.send") # This does: mocker.patch("myapp.orders.send")
Patch the location where the name is looked up, which is usually the module that uses it.
Forgetting autospec
Without autospec, a mock accepts any arguments, so a test can pass while the real function signature has drifted:
mocker.patch("myapp.orders.send", autospec=True)
With autospec, calling the mock with the wrong signature raises TypeError, which catches mismatches between the test and the real API.
Asserting on the wrong object
If you patch a method on a class but the code under test creates its own instance, the mock lives on the class, not on the instance. Assert on the class-level mock, or patch the instance method after the instance is created.
Over-specifying calls
Asserting the exact argument list for every call makes tests brittle. If an argument is irrelevant to the behavior under test, assert only the parts that matter, or use assert_has_calls with the relevant subset.
Maintainability: keeping mocks from leaking into tests
The main maintainability concern with mocker is over-mocking. When a test patches many dependencies, it stops testing real behavior and starts testing that the mocks were configured correctly. A test that asserts five calls on three mocks often breaks when the implementation changes in ways that do not affect behavior.
Keep patches narrow. Patch only the dependency the code under test actually touches. If the code under test calls a function that internally calls another external service, patch the outer function, not the inner one, unless the test specifically targets the inner behavior.
Use autospec for any dependency with a stable public signature. It turns signature drift into an immediate failure instead of a silent mismatch.
Prefer spies over patches for internal helpers that are deterministic. A spy keeps the real logic in the test while still verifying the call, which reduces the amount of mock configuration you have to maintain.
Finally, mocker cleans up automatically. You do not need to call mocker.stopall() in a finally block, and doing so manually can mask which patch leaked when a test fails. Let the fixture handle teardown so failures surface with the original traceback.