Вход на сайт

Просмотр новости

Найдите то, что Вас интересует

Spy on Function Calls With unittest.mock Wraps

Дата публикации: 09-08-2026 20:25:25



Основное содержимое страницы с новостью.

2026-07-25This magnetic magnifying glass is all the rage for spying, or so I’m told.

Testing terminology distinguishes between different kinds of test doubles, including:

  • mocks, which replace the real behaviour of a function.
  • spies, which wrap a function, recording calls for later assertions.

Despite its name, unittest.mock is not just for mocks: it can also create spies, through the wraps argument of its Mock classes. Calls to such a Mock pass through to the wrapped object and return its real results, while the mock records the calls for later assertions.

For example, say we wanted to test the caching in this module, which computes the sound that an owl makes based on its name:

sound_cache: dict[str, str] = {}


def owl_sound(name: str) -> str:
    if name not in sound_cache:
        sound_cache[name] = compute_owl_sound(name)
    return sound_cache[name]


def compute_owl_sound(name: str) -> str:
    firsts = ("Hoo", "Hoot", "Toot", "Hooo")
    seconds = ("hoo", "hoot", "toot", "hooo")
    first = firsts[len(name) % len(firsts)]
    second = seconds[ord(name[0]) % len(seconds)]
    return f"{first}-{second}!"

owl_sound() stores results in the module-level sound_cache dictionary, calling compute_owl_sound() only for names it hasn’t seen before. To test that caching, we can spy on compute_owl_sound() and check how many times it gets called:

from unittest import TestCase, mock

import example
from example import owl_sound


class OwlSoundTests(TestCase):
    def setUp(self):
        example.sound_cache.clear()

    def test_owl_sound_caches(self):
        with (
            mock.patch.object(
                example,
                "compute_owl_sound",
                autospec=True,
                wraps=example.compute_owl_sound,
            ) as spy,
        ):
            first = owl_sound("athena")
            second = owl_sound("athena")

        assert first == "Toot-hoot!"
        assert second == "Toot-hoot!"
        assert spy.mock_calls == [mock.call("athena")]

mock.patch.object() replaces compute_owl_sound in the example module with a Mock. patch.object(), like plain patch(), passes extra keyword arguments to the mock that it creates, so wraps=example.compute_owl_sound makes that mock wrap the real function.

The test asserts on the two results from calling owl_sound(), then makes an assertion with the “spy” mock to check that compute_owl_sound() was called only once.

autospec=True is optional for spying on functions, but required for spying on methods, so it’s best to pass it. And indeed, it’s generally a good idea to use this feature, known as “autospeccing”, whenever you create a mock, because it makes the mock copy the signature of the wrapped object and thus fail when invalid arguments are passed.

Configured behaviour beats wraps

wraps only provides the mock’s default behaviour, so other configuration takes precedence. If you set return_value, the wrapped object is not called at all:

>>> spy = mock.Mock(wraps=compute_owl_sound, return_value="dude, whatever")
>>> spy("athena")
'dude, whatever'

And if you set side_effect, it runs instead of the wrapped object, unless it returns mock.DEFAULT, in which case the call passes through:

>>> def noisy(name):
...     print(f"🦉 compute_owl_sound called with {name!r}")
...     return mock.DEFAULT
...
>>> spy = mock.Mock(wraps=compute_owl_sound, side_effect=noisy)
>>> spy("athena")
🦉 compute_owl_sound called with 'athena'
'Toot-hoot!'

This combination lets you intercept calls while still delegating to the real object, at least sometimes.

In pytest

If you use pytest to run your tests, the above patterns all work. The pytest-mock plugin also provides mocker.spy(), a convenient shortcut built on wraps and autospec:

import example
from example import owl_sound


def test_owl_sound_caches(mocker):
    mocker.patch.dict(example.sound_cache, clear=True)
    spy = mocker.spy(example, "compute_owl_sound")

    first = owl_sound("athena")
    second = owl_sound("athena")

    assert first == "Toot-hoot!"
    assert second == "Toot-hoot!"
    assert spy.mock_calls == [mocker.call("athena")]

It additionally records the latest return value on spy.spy_return, all return values on spy.spy_return_list, and any exception raised on spy.spy_exception.

Simplify with a spy() helper

mocker.spy() is neat, but if you’re not using pytest or pytest fixtures, try the below helper instead. This spy() context manager reduces the boilerplate of calling mock.patch.object() with the same wraps and autospec arguments each time:

from contextlib import contextmanager
from typing import Any, Generator
from unittest import mock


@contextmanager
def spy(obj: Any, name: str) -> Generator[Any]:
    """
    Spy on the named function or method of obj: calls pass through to
    the real object whilst being recorded on the yielded mock.

    Source: https://adamj.eu/tech/2026/07/25/python-spy-unittest-mock-wraps/
    """
    with mock.patch.object(
        obj, name, autospec=True, wraps=getattr(obj, name)
    ) as spy_mock:
        yield spy_mock

Use it like so (here it has been placed in a testing module):

from unittest import TestCase, mock

import example
from example import owl_sound
from testing import spy


class OwlSoundTests(TestCase):
    def setUp(self):
        example.sound_cache.clear()

    def test_owl_sound_caches(self):
        with spy(example, "compute_owl_sound") as compute_spy:
            first = owl_sound("athena")
            second = owl_sound("athena")

        assert first == "Toot-hoot!"
        assert second == "Toot-hoot!"
        assert compute_spy.mock_calls == [mock.call("athena")]

Read my book Boost Your Django DX, freshly updated in November 2024.


One summary email a week, no spam, I pinky promise.

Related posts:

Tags: python, unittest

Схожие новости

#Наименование новостиТональностьИнформативностьДата публикации
1pytest-tia: Run Only the Tests Your Git Diff Actually Affects02012-07-2026
2pytest-mrt: Catch Database Migration Rollback Failures026.6702-07-2026
3django-orm-lens: Django Schema Review03510-08-2026
4Find All Instances of a Class With gc.get_objects()01002-08-2026
50016-02-2026
60016-02-2026
70016-02-2026
8Смычки для альтов01010-07-2026
9crawlee 1.8.4b10520-07-2026
100002-04-2026

Классификация: . Схожих патентов: 0. Схожих новостей: 10. Тональность: 0. Информативность: 10. Источник: pythondigest.ru.