Featured image of post pytest-mock: Cleaner Mocking With the mocker Fixture

pytest-mock: Cleaner Mocking With the mocker Fixture

pytest-mock provides a mocker fixture that integrates unittest.mock.patch into pytest's lifecycle β€” no manual start/stop, no with blocks, no decorator ordering issues. spy preserves real behavior while tracking calls. stub tests callbacks.

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-cleanup
  • mocker.spy β†’ real behavior preserved + call tracking
  • mocker.stub β†’ lightest-weight fake callback

Combined with pytest fixtures and polyfactory for test data, test setup overhead gets very low.

References