Espiar sin reemplazar con mocker.spy

Publicado: 12 de febrero de 2026


A veces querés verificar que una función fue llamada con los argumentos correctos sin reemplazarla por un fake. Para eso existe mocker.spy.

Donde mocker.patch reemplaza la función real por un MagicMock, mocker.spy envuelve la función real para que siga ejecutándose normalmente — pero el wrapper registra cada llamada, argumento y valor de retorno.


1. Ejemplo de código

converter.py

def celsius_to_fahrenheit(c):
    return round(c * 9 / 5 + 32, 2)

weather.py

from converter import celsius_to_fahrenheit


def format_forecast(city, temp_c):
    temp_f = celsius_to_fahrenheit(temp_c)
    return f"{city}: {temp_c}\u00b0C / {temp_f}\u00b0F"

weather.py hace from converter import celsius_to_fahrenheit, lo que crea un name binding local dentro del módulo weather.

2. ¿Qué se debe espiar?

La misma regla de “parchear donde se usa” aplica a los espías.

mocker.spy(obj, name) funciona con setattr — reemplaza obj.name con un wrapper que delega a la función original. Si el módulo que llama importó la función con from X import Y, el name binding vive en el namespace del módulo que llama, no en el módulo original.

Correcto — espiar donde la función se usa:

mocker.spy(weather, "celsius_to_fahrenheit")

Incorrecto — espiar donde la función se define:

mocker.spy(converter, "celsius_to_fahrenheit")

El objetivo incorrecto reemplaza el atributo en converter, pero weather.celsius_to_fahrenheit sigue apuntando directamente a la función sin envolver — el espía no ve nada.

3. Pruebas

test_weather.py

from weather import format_forecast


def test_spy_where_used(mocker):
    """Spy on the name inside the module that calls it."""
    import weather

    spy = mocker.spy(weather, "celsius_to_fahrenheit")

    result = format_forecast("Buenos Aires", 25)

    assert result == "Buenos Aires: 25\u00b0C / 77.0\u00b0F"  # real function ran
    spy.assert_called_once_with(25)  # spy tracked the call
    assert spy.spy_return == 77.0  # spy recorded return value


def test_spy_wrong_target(mocker):
    """Spy on the definition site \u2014 misses calls through weather module."""
    import converter

    spy = mocker.spy(converter, "celsius_to_fahrenheit")

    result = format_forecast("Buenos Aires", 25)

    assert result == "Buenos Aires: 25\u00b0C / 77.0\u00b0F"  # function still works
    assert spy.call_count == 0  # spy saw nothing

Salida de pruebas

============================= test session starts ==============================
platform darwin -- Python 3.13.0, pytest-9.0.2, pluggy-1.6.0 -- /usr/local/opt/python@3.13/bin/python3.13
rootdir: /mocker-spy-python/sandbox
plugins: mock-3.15.1
collecting ... collected 2 items

test_weather.py::test_spy_where_used PASSED                              [ 50%]
test_weather.py::test_spy_wrong_target PASSED                            [100%]

============================== 2 passed in 0.03s ===============================

La primera prueba espía sobre el objetivo correcto y verifica que la llamada fue registrada. La segunda muestra que espiar sobre el sitio de definición no detecta la llamada — spy.call_count queda en cero.

4. Atributos específicos del espía

Además de los asserts estándar de MagicMock (assert_called_once_with, call_args, call_count, …), un espía agrega:

  • spy.spy_return — el valor de retorno de la última llamada real.
  • spy.spy_exception — la excepción lanzada por la última llamada real (o None).

Estos permiten inspeccionar qué hizo la función real, no solo que fue llamada.