¿Cómo funciona mocker en Python?
Respuesta corta:
mocker es un fixture de pytest proporcionado por el plugin pytest-mock.
Entrega un pequeño helper (normalmente llamado mocker) que envuelve a unittest.mock de la biblioteca estándar, para que puedas:
- Reemplazar (“parchear”) objetos reales (funciones, métodos, atributos) con
MagicMockcontrolables. - Registrar llamadas: verificar cuántas veces se invocó un doble, qué argumentos recibió y más detalles.
- Deshacer todos los parches automáticamente al final de cada prueba para que tu código vuelva a su estado original.
1. Ejemplo de código
email_gateway.py
import smtplib
from email.message import EmailMessage
def send_email(recipient: str, subject: str, body: str) -> str:
"""Deliver transactional email through our SMTP relay."""
# deliver a message via SMTP
message = EmailMessage()
message["From"] = "noreply@example.com"
message["To"] = recipient
message["Subject"] = subject
message.set_content(body)
try:
with smtplib.SMTP("mail.example.com", 587, timeout=3) as client:
client.starttls()
client.login("noreply@example.com", "super-secret")
client.send_message(message)
except OSError:
# Swallow transport errors for this illustrative example.
pass
return f"sent:{recipient}:{subject}"
email_service.py
from email_gateway import send_email
def notify_user(recipient: str, message: str) -> str:
"""Send the standard account-update email copy to a user."""
subject = "Account update"
body = f"Hello!\n\n{message}"
return send_email(recipient, subject, body)
Como la función de alto nivel notify_user importa send_email y cierra sobre ese nombre, las pruebas parchean email_service.send_email en lugar de email_gateway.send_email.
2. ¿Cuál debería ser el objetivo?
Regla general:
Parchea el nombre visible dentro del módulo bajo prueba, no el lugar original donde se definió el objeto. En otras palabras, parchea “donde se usa” en lugar de “donde se definió”.
3. Un PocketMocker mínimo
Ahora construyamos una versión mini de mocker para mostrar todas las piezas en movimiento.
No tocaremos unittest.mock.patch; en su lugar haremos el trabajo manualmente con:
importlib.import_modulegetattr/setattrunittest.mock.MagicMock
pocket_mocker.py
import importlib
from unittest.mock import MagicMock
class PocketMocker:
"""Tiny helper to mimic the patching API readers see from pytest-mock."""
def __init__(self):
self._stack = []
def patch(self, target: str, replacement=None, **kwargs):
module_path, attribute = target.rsplit(".", 1)
module = importlib.import_module(module_path)
original = getattr(module, attribute)
if replacement is None:
replacement = MagicMock(**kwargs)
setattr(module, attribute, replacement)
self._stack.append((module, attribute, original))
return replacement
def stopall(self):
while self._stack:
module, attribute, original = self._stack.pop()
setattr(module, attribute, original)
Explicación de los helpers de bajo nivel
-
importlib.import_module(module_path)- Recibe una cadena como
"email_service.send_email"y devuelve el objeto de módulo real. - Se apoya en el sistema de importación de Python y en
sys.modules.
- Recibe una cadena como
-
getattr(module, attr_name)- Lee un atributo de un objeto.
- Equivalente a
module.attr_name.
-
setattr(module, attr_name, replacement)- Asigna un atributo sobre un objeto.
- Equivalente a
module.attr_name = replacement. - Este es el paso literal de “patch”: sobrescribimos la función original con nuestro doble.
-
MagicMock(**kwargs)-
Crea un objeto falso flexible que:
- Registra llamadas (para que puedas hacer asserts luego).
- Puede configurarse con
return_valueoside_effect. - Soporta atributos y métodos dinámicos.
-
Construir PocketMocker demuestra que pytest-mock y unittest.mock son simplemente una capa más agradable sobre este mismo comportamiento base.
Pruebas
test_email_service.py
import email_service
from pocket_mocker import PocketMocker
def test_notify_user_with_pytest_mocker(mocker):
mocker.patch(
"email_service.send_email", return_value="sent:me@example.com:Account update"
)
result = email_service.notify_user("me@example.com", "You have a new badge!")
assert result == "sent:me@example.com:Account update"
def test_notify_user_with_pocket_mocker():
pocket = PocketMocker()
pocket.patch(
"email_service.send_email", return_value="sent:you@example.com:Account update"
)
result = email_service.notify_user("you@example.com", "Password changed")
assert result == "sent:you@example.com:Account update"
pocket.stopall()
Salida de pruebas
test_email_service.py ..
============================= 2 passed in 0.05s =============================
La primera prueba ejercita el fixture real de pytest-mock, mientras que la segunda recorre la implementación casera para que puedas seguir cada paso.
Research Notes
Despliega para leer las notas de investigación, referencias y comparaciones reunidas para este tema.