pytest-mock
pytest-mock is a pytest plugin that exposes one fixture, mocker, wrapping the standard library's unittest.mock. Every patch you make through it is undone when the test finishes, so you never write a with block or stack patch decorators. The fixture mirrors the mock API you already know (mocker.patch, patch.object, patch.dict, patch.multiple) and re-exports the useful names as attributes, so mocker.MagicMock, mocker.AsyncMock, mocker.PropertyMock, mocker.ANY, mocker.call, and mocker.sentinel are all there without a second import. On top of that it adds two things mock does not have: mocker.spy, which wraps a real function so it still runs while recording calls, return values and exceptions, and mocker.stub, a named throwaway callable for testing callbacks. It also patches mock's assertion failures to use pytest's diff output, so a failed assert_called_once_with shows you which argument differed.
A small, stable convenience layer that removes the nesting and decorator ordering pain from unittest.mock, and mocker.spy alone justifies the install. It will not make heavily mocked tests good tests, and if you are only patching one name per test the standard library is already enough.
Use it if
- A single test needs several patches: mocker.patch called three times reads better than three nested with blocks or three stacked decorators whose argument order you have to keep straight
- You want to assert on real code without replacing it: mocker.spy runs the original function and still gives you call_count, spy_return, spy_return_list, and spy_exception
- You are mixing mocks with parametrize or other fixtures, where mock.patch decorators inject positional arguments that collide with pytest's own fixture arguments
- You want readable failures: the plugin hides mock's internal frames and runs pytest's assertion rewriting on the expected and actual call arguments
- You need a mock that outlives one test: class_mocker, module_mocker, package_mocker, and session_mocker are the same API at wider scopes
- You only patch one thing per test: `with mock.patch(...)` is stdlib, costs no dependency, and reads fine at that size
- You just need to set an attribute, an env var, or a dict key: pytest's built-in monkeypatch fixture already does that with automatic teardown and no extra install
- You are mocking HTTP calls: patching requests or httpx by hand is fragile, and responses or respx intercept at the transport layer so your production code path stays intact
- You expect it to solve the actual hard part: patching the wrong name is the number one mocking bug, and mocker.patch takes the same string target as mock.patch, so it gets it wrong in exactly the same way
- You want to use it as a context manager or decorator: the fixture deliberately warns when you do, because that pattern defeats the whole point of the plugin
- Heavy mocking is the smell, not the fix: every patched name couples the test to an implementation detail, and a fake object or a real in-memory dependency usually survives refactoring better
Setup reality
pip install pytest-mock and it registers itself through pytest's entry point system, so the mocker fixture appears with no conftest.py edit and no plugin list. It needs pytest 6.2.5 or newer and Python 3.9 or newer on 3.15.1, with 3.9 dropped in the unreleased branch. Two behaviours catch people out. The plugin monkeypatches the mock library to shorten tracebacks, which is automatically disabled under --tb=native and can be turned off with mock_traceback_monkeypatch = false in pytest.ini if it ever confuses your reporting. And 3.15.1 changed mocker.spy: duplicating an iterator return value now requires an explicit duplicate_iterators=True, so code relying on 3.15.0's automatic behaviour needs the flag. If you would rather use the standalone mock package from PyPI than the stdlib module, set mock_use_standalone_module = true. Type annotations are shipped and checked with mypy only; other type checkers are untested.
Patterns
Replace a function for one testpatch-a-function
def test_removes_file(mocker):
remove = mocker.patch("myapp.storage.os.remove")
storage.delete("/tmp/report.csv")
remove.assert_called_once_with("/tmp/report.csv")The patch is reverted at test teardown with no with block. mocker.patch returns the MagicMock, so grab the return value instead of reaching back through the module.
Patch the name the code under test looks upwhere-to-patch
# myapp/service.py
from myapp.clients import send_email
# Wrong: the import above already bound the original
mocker.patch("myapp.clients.send_email")
# Right: patch the name inside the module that uses it
mocker.patch("myapp.service.send_email")This is the mistake behind most reports that mocking is not working. `from x import y` copies the reference at import time, so patching x.y afterwards leaves the copy in place. Patch where the name is used, not where it is defined.
Catch wrong call signatures with autospecautospec-a-method
def test_charge(mocker):
charge = mocker.patch.object(
PaymentGateway, "charge", autospec=True, return_value="ok"
)
assert PaymentGateway().charge(1000) == "ok"
charge.assert_called_once()Without autospec a MagicMock accepts any arguments, so a test keeps passing after you rename a parameter in production code. With autospec=True the mock raises TypeError on a bad call, and the mock receives self as its first argument in the recorded call.
Fake results, sequences, and failuresreturn-value-and-side-effect
def test_retry(mocker):
fetch = mocker.patch("myapp.client.fetch")
fetch.return_value = {"ok": True}
fetch.side_effect = [TimeoutError(), TimeoutError(), {"ok": True}]
assert client.fetch_with_retry() == {"ok": True}
assert fetch.call_count == 3side_effect wins over return_value. A list is consumed one call at a time and raises StopIteration when it runs dry, an exception class or instance is raised, and a callable is invoked with the real arguments and its result returned.
Watch a function without replacing itspy-on-real-code
def test_cache_is_used(mocker):
spy = mocker.spy(pricing, "compute_total")
assert checkout(cart) == 4200
assert spy.call_count == 1
assert spy.spy_return == 4200
assert spy.spy_return_list == [4200]
assert spy.spy_exception is NoneThe original function still runs, so this tests behaviour rather than a stand-in. Use spy_return and spy_exception, not return_value and side_effect, which mean something different on a MagicMock. spy works on plain functions, methods, classmethods, staticmethods, and async def.
Mock a coroutine functionasync-mock
@pytest.mark.asyncio
async def test_fetch(mocker):
fetch = mocker.patch("myapp.api.fetch", return_value={"id": 1})
assert await api.fetch("/users/1") == {"id": 1}
fetch.assert_awaited_once_with("/users/1")mock detects that the target is an async def and creates an AsyncMock automatically, so awaiting the patched name works with no extra flag. Use assert_awaited_once_with rather than assert_called_once_with when you want to prove the coroutine was actually awaited and not just created.
Replace a property with a controlled valuemock-a-property
def test_expired_token(mocker):
is_expired = mocker.patch.object(
Session, "is_expired", new_callable=mocker.PropertyMock,
return_value=True,
)
assert Session().needs_refresh() is True
is_expired.assert_called_once_with()PropertyMock has to be patched on the class, never the instance, and it records reads as calls with no arguments. Setting return_value directly on a plain mocker.patch of a property gives you a Mock object instead of the value.
Override environment variables or a dictpatch-dict-and-env
def test_reads_api_key(mocker):
mocker.patch.dict(os.environ, {"API_KEY": "test-key"}, clear=False)
assert config.load().api_key == "test-key"clear=True wipes the rest of the dict first, which is how you prove a code path does not silently pick up a developer's real credentials. For env vars alone, pytest's monkeypatch.setenv is a smaller tool that does the same job.
Assert a callback was invokedstub-a-callback
def test_emits_progress(mocker):
on_progress = mocker.stub(name="on_progress")
downloader.run(callback=on_progress)
on_progress.assert_any_call(100, 100)A stub accepts any arguments and shows its name in the repr, which makes failures readable when several stubs are in play. Use mocker.async_stub for a callback that gets awaited.
Un-patch partway through a teststop-a-mock-early
def test_only_first_call_is_faked(mocker):
spy = mocker.spy(Report, "render")
Report().render()
assert spy.call_count == 1
mocker.stop(spy)
Report().render() # real method, not recorded
assert spy.call_count == 1mocker.stop works on anything mocker.patch or mocker.spy returned. mocker.resetall() is the other half: it clears recorded calls on every mock so far without removing the patches.
Patch once for a whole module or sessionwider-scope-mocker
@pytest.fixture(scope="session", autouse=True)
def no_real_network(session_mocker):
session_mocker.patch(
"socket.socket.connect",
side_effect=RuntimeError("no network in tests"),
)The function-scoped mocker cannot be requested from a wider-scoped fixture, which is why class_mocker, module_mocker, package_mocker and session_mocker exist. Session-wide patches leak across every test, so keep them to guardrails like this one.
Type-annotate tests that use mockerannotate-mocker
from pytest_mock import MockerFixture, MockType
def test_invoice(mocker: MockerFixture) -> None:
send: MockType = mocker.patch("billing.send_invoice")
billing.run()
send.assert_called_once()MockType and AsyncMockType arrived in 3.14 and SpyType for mocker.spy results is in the unreleased branch. The annotations are verified against mypy only, so other type checkers may report differences the maintainers are not tracking.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| mock | PyPI | You want the newest unittest.mock features on an older Python, or you are happy with plain with blocks and decorators |
| responses | PyPI | The thing you are mocking is an HTTP call made with requests, and you want to match on URL and body instead of patching a function name |
| respx | PyPI | Same idea for httpx, including async clients, with route matching and recorded request assertions |