Using unittest.mock.patch directly has a few rough edges.
With the decorator form, argument order is counterintuitive:
1
2
3
4
| @patch('module.ClassB')
@patch('module.ClassA')
def test_something(mock_a, mock_b): # reversed from decorator order
...
|
With the context manager form, multiple patches mean nesting:
1
2
3
4
| def test_something():
with patch('module.A') as mock_a:
with patch('module.B') as mock_b:
...
|
pytest-mock’s mocker fixture cleans this up: patches are automatically reverted when the test ends, no context manager or decorator needed, mock objects come back directly.
Install
1
| pip install pytest-mock
|
Basic Patching
1
2
3
4
5
6
| def test_send_email(mocker):
mock_smtp = mocker.patch('myapp.email.smtplib.SMTP')
send_welcome_email('user@example.com')
mock_smtp.return_value.send_message.assert_called_once()
|
mocker.patch() works the same as unittest.mock.patch, but without with or @. Automatically unpatched when the test ends.
Patching an Object’s Method
1
2
3
4
5
6
| def test_save(mocker):
mock_save = mocker.patch.object(UserRepository, 'save')
service.create_user('Alice')
mock_save.assert_called_once()
|
mocker.patch.object(TargetClass, 'method_name') is less error-prone than spelling out the full dotted path.
Return Values and side_effect
1
2
3
4
5
| def test_get_user(mocker):
mocker.patch('myapp.db.find_user', return_value={'id': 1, 'name': 'Alice'})
result = get_user(1)
assert result['name'] == 'Alice'
|
1
2
3
4
5
6
7
8
| def test_retry_on_error(mocker):
mocker.patch(
'myapp.api.fetch',
side_effect=[ConnectionError(), ConnectionError(), {'data': 'ok'}]
)
result = fetch_with_retry()
assert result == {'data': 'ok'}
|
side_effect with a list returns each item in sequence β useful for simulating failures followed by success.
Patch Location Matters
Patch where it’s used, not where it’s defined.
1
2
3
4
5
| # myapp/notifications.py
from myapp.email import send_email # imported here
def notify_user(user):
send_email(user['email']) # used under this name
|
1
2
3
4
5
| # Wrong: patching where it's defined
mocker.patch('myapp.email.send_email')
# Correct: patching where it's used
mocker.patch('myapp.notifications.send_email')
|
This is one of the most common mock mistakes. See Python mock: where to patch for a detailed explanation.
spy: Track Calls Without Replacing the Real Behavior
mocker.patch replaces the target entirely. mocker.spy preserves the original logic while tracking calls, return values, and exceptions.
1
2
3
4
5
6
7
8
9
10
11
| def calculate_tax(amount):
return amount * 0.1
def test_tax_called(mocker):
spy = mocker.spy(myapp.tax, 'calculate_tax')
result = process_order(amount=1000)
spy.assert_called_once_with(1000)
assert spy.spy_return == 100.0 # original function actually ran
assert result == 1100.0 # business logic is correct too
|
Use spy when you want to verify a function was called without faking its behavior. With mocker.patch, you’d have to set return_value yourself to simulate the real output β spy skips that.
spy Attributes
1
2
3
4
5
6
| spy.assert_called_once()
spy.assert_called_with(arg1, arg2)
spy.call_count # number of calls
spy.spy_return # return value from last call
spy.spy_return_list # all return values (v3.13+)
spy.spy_exception # last exception raised
|
Async Functions
1
2
3
4
| async def test_async(mocker):
spy = mocker.spy(myapp, 'async_fetch')
await fetch_data()
spy.assert_called_once()
|
stub: A Lightweight Fake Callback
A stub accepts any arguments and records calls β useful when testing that a callback was invoked:
1
2
3
4
5
6
| def test_callback(mocker):
callback = mocker.stub(name='on_success')
do_something(on_success=callback)
callback.assert_called_once_with({'status': 'ok'})
|
resetall and stopall
1
2
3
4
5
6
7
8
9
| def test_something(mocker):
mock_a = mocker.patch('myapp.A')
mock_b = mocker.patch('myapp.B')
# reset call history on all mocks (patches stay active)
mocker.resetall()
# manually stop all patches (usually not needed β auto-cleaned at test end)
mocker.stopall()
|
resetall() is useful when you need to assert on two separate phases of a test independently.
Different Scopes
The default mocker is function-scoped. For class or module scope:
1
2
3
4
5
6
| @pytest.fixture(scope="module")
def patched_env(module_mocker):
module_mocker.patch.dict('os.environ', {'API_KEY': 'test-key'})
def test_a(patched_env): ...
def test_b(patched_env): ... # same module, same patch
|
Compared to unittest.mock Directly
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| # unittest.mock
from unittest.mock import patch
def test_something():
with patch('myapp.service.fetch') as mock_fetch:
mock_fetch.return_value = {'data': 'ok'}
result = do_something()
mock_fetch.assert_called_once()
# pytest-mock
def test_something(mocker):
mock_fetch = mocker.patch('myapp.service.fetch', return_value={'data': 'ok'})
result = do_something()
mock_fetch.assert_called_once()
|
One less indentation level, return_value set inline, mock object returned directly.
The difference is bigger with multiple patches:
1
2
3
4
5
6
7
8
9
10
11
12
13
| # unittest.mock
def test_something():
with patch('myapp.A') as mock_a:
with patch('myapp.B') as mock_b:
with patch('myapp.C') as mock_c:
...
# pytest-mock
def test_something(mocker):
mock_a = mocker.patch('myapp.A')
mock_b = mocker.patch('myapp.B')
mock_c = mocker.patch('myapp.C')
...
|
Summary
pytest-mock doesn’t add new capabilities β it makes unittest.mock fit naturally into pytest:
mocker.patch β no with / decorator, auto-cleanupmocker.spy β real behavior preserved + call trackingmocker.stub β lightest-weight fake callback
Combined with pytest fixtures and polyfactory for test data, test setup overhead gets very low.
References