Dobles de prueba y costuras
Reemplaza en una prueba una dependencia lenta o que no controlas, sin esconder el comportamiento que querías comprobar.
AL TERMINAR, PODRÁS
- Elegir entre un stub, un fake, un spy y un mock para una dependencia
- Darle al código una costura para que una prueba le pase su propio colaborador
- Reconocer cuándo un doble deja pasar una prueba mientras producción falla
Descripción del diagrama: Worker → Reloj (now()); Worker → Repositorio (get / save); Worker → Correo (send()); Worker → Pagos (charge()); Pruebas de contrato → Repositorio (lo mantiene fiel)
Por qué reemplazar a un colaborador
El worker de Relay lee el reloj, carga trabajos de una base de datos y envía correos a través de un proveedor.
En una prueba unitaria (test) las versiones reales estorban: el proveedor es lento, puede costar dinero, envía mensajes de verdad y puede fallar por razones que no tienen que ver con tu código. Un doble de prueba (test double) es cualquier objeto que ocupa el lugar de una dependencia real durante una prueba. Los tipos más comunes se distinguen por lo que hacen.
Un dummy llena un parámetro y nunca se usa. Un stub devuelve respuestas preparadas para la prueba, como un reloj que siempre marca las 10:00. Un fake funciona de verdad, pero con un atajo que lo descarta para producción, como un repositorio guardado en un diccionario.
Un spy (espía) se comporta como un stub y además registra cómo lo llamaron. Un mock se configura con las llamadas que espera y hace fallar la prueba si no ocurren. Elige según lo que quieras comprobar: el estado resultante (un trabajo marcado como terminado) o una interacción que es en sí el comportamiento (un correo enviado).
class RecordingMailer:
def __init__(self):
self.sent = []
def send(self, to, subject):
self.sent.append((to, subject))
Dale al código una costura
Una costura (seam) es un punto donde una prueba puede cambiar lo que usa el código sin editar ese código.
En Python, la costura más simple es un parámetro: pasa el reloj, el repositorio y el servicio de correo desde fuera, por la función o el constructor, en lugar de crearlos adentro. Eso es inyección de dependencias (dependency injection), y no necesita ningún framework. El código que llama a time.time() o crea su propio cliente de correo no tiene costura, así que sus pruebas recurren a unittest.mock.patch, que cambia un nombre mientras dura la prueba.
Reemplaza el nombre donde lo busca el código que pruebas, no donde está definido, o el cambio no surte efecto. Parchear funciona, pero ata cada prueba a la forma en que un módulo importa las cosas. Cuando el código es tuyo, un parámetro suele ser más claro: la prueba se lee como una llamada normal y las dependencias se ven en la firma.
def test_finishing_a_job_notifies_its_owner():
jobs = InMemoryJobs()
jobs.add({"id": "j1", "owner": "ana@example.com", "state": "running"})
mailer = RecordingMailer()
worker = Worker(jobs, mailer, clock=lambda: 1_000)
worker.finish("j1")
assert jobs.get("j1")["state"] == "done"
assert mailer.sent == [("ana@example.com", "Job j1 finished")]
Cuando un doble miente
Un doble codifica lo que crees de la dependencia real.
Si esa creencia está equivocada, la prueba pasa y producción falla. Un repositorio fake que sobrescribe en silencio un id repetido esconde el error de restricción de unicidad que lanza la base de datos real. Un Mock simple acepta cualquier nombre de método, así que sigue pasando aunque el cliente real cambie charge por create_charge.
Tres hábitos ayudan. Construye los mocks a partir del objeto real, con spec o create_autospec, para que un atributo que falta o una firma equivocada hagan fallar la prueba. Ejecuta las mismas pruebas de contrato contra el fake y contra la implementación real, para que el fake siga siendo fiel.
Envuelve el cliente de un tercero en un adaptador pequeño que sea tuyo y reemplaza el adaptador, no la biblioteca. Por último, no compruebes cada llamada interna: una prueba que verifica qué funciones auxiliares se ejecutaron se rompe con un refactor inofensivo y aun así no detecta un resultado incorrecto.
from unittest.mock import create_autospec
payments = create_autospec(PaymentClient, instance=True)
worker = Worker(jobs, mailer, clock=lambda: 1_000, payments=payments)
worker.finish("j1")
payments.charge.assert_called_once_with("ana@example.com", 500)
ERROR FRECUENTE
Simular con mocks todo lo que toca el código. La prueba repite entonces la implementación llamada por llamada, falla con cada refactor y pasa incluso cuando los colaboradores reales rechazarían lo que el código envía.
INTÉNTALO TÚ
Enumera los colaboradores del worker de Relay: el reloj, el repositorio de trabajos, el servicio de correo y el cliente de pagos. Para cada uno, elige el doble que usarías en una prueba unitaria y di en qué podría diferir del real.
Decide en el laboratorio.
El checkpoint de dos preguntas, tus notas y tu progreso guardado están en el laboratorio. Gratis y sin cuenta.
Abrir en el laboratorio ↗CONÉCTALO CON RELAY
Pasa el reloj, el repositorio de trabajos y el servicio de correo al worker de Relay desde fuera. Escribe un repositorio en memoria y un servicio de correo que registre los envíos, y ejecuta un mismo conjunto de pruebas de contrato contra el repositorio en memoria y contra el de SQLite.