Un test para place_order necesita una pasarela de pago, un cliente de SMS y una calculadora de impuestos. Conecta las versiones reales y cada ejecución del test cobra una tarjeta y manda un SMS a un número real. Un test unitario tiene que correr en milisegundos y sin red, así que las tres dependencias hay que sustituirlas por otra cosa. Ese “otra cosa” llega en cinco variantes. Cada una resuelve un problema distinto, y tratarlas como intercambiables explica por qué las discusiones de “pon un mock y ya” en code review no llegan a ningún lado.
Qué es un test double
Gerard Meszaros acuñó el término “test double” en xUnit Test Patterns como el término genérico para cualquier cosa que sustituye a una dependencia real durante un test, igual que un doble de riesgo sustituye a un actor. Los equipos de rodaje usan dobles distintos según la toma: uno solo tiene que quedarse de fondo con el peinado correcto, otro hace de verdad el choque de coches. Los test doubles se dividen igual. Dummy, stub, spy, fake y mock son cinco trabajos distintos, y lo que los separa es qué comprueba el test, no cuán elaborado sea el objeto sustituto.
Esta es la clase que prueba cada ejemplo siguiente. Tres de sus dependencias merece la pena sustituirlas; la cuarta resulta no importar en absoluto:
| |
logger solo se ejecuta en el camino de error. Eso es clave para el primer ejemplo.
Dos sustitutos pequeños se reutilizan en los próximos tests, así que se definen una sola vez aquí:
| |
Qué es un objeto dummy en testing
Un dummy se pasa solo para satisfacer un parámetro que el test nunca ejercita. No hace nada y no comprueba nada. El test del camino feliz de abajo nunca llega a la rama de la excepción, así que logger puede ser cualquier cosa que sobreviva a que se le asigne un atributo:
| |
object() funciona porque nadie le llama ningún método. Si un cambio posterior hiciera que place_order también registrara un log en el camino de éxito, este test fallaría con un AttributeError. Ese fallo es útil: te dice que el dummy dejó de ser un dummy.
Qué es un stub en testing unitario
Un stub devuelve una respuesta fija para que el resto del test siga siendo determinista. La calculadora de impuestos real podría llamar a una API de tasas fiscales. El test no quiere eso, así que StubTaxCalculator devuelve 108.00 sea cual sea el subtotal que recibe. Ya lo has visto funcionando en el test del dummy de arriba, junto con StubGateway haciendo el mismo trabajo para charge.
Nada comprueba si se llamó a total_with_tax, ni con qué argumento. Un stub le da al código bajo test una entrada controlada; nunca verifica una interacción. Esa es la línea que separa un stub de los otros tres doubles.
Qué es un test spy
Un spy registra las llamadas que recibe para que el test pueda inspeccionarlas después de que el código bajo test se haya ejecutado. unittest.mock.Mock registra automáticamente cada llamada, así que la mayoría de los suites en Python lo usan como spy aunque la palabra nunca aparezca en el test:
| |
No se configuró nada en sms_spy antes de la llamada. Aceptó lo que le dieron, lo registró, y el test lo interrogó después.
Qué es un fake object en testing
Un stub no puede decirte si dos cobros seguidos obtienen dos IDs distintos, porque solo conoce un valor. Cuando un test necesita comportamiento a través de varias llamadas, lo que hace falta es un fake: una implementación pequeña que funciona de verdad.
| |
| |
FakeGateway no hace ninguna llamada de red ni tiene un procesador de pagos detrás, pero mantiene estado igual que lo haría el real. El test necesitaba exactamente ese estado, y un stub no podía darlo.
Qué es un mock object
En la definición de Meszaros, un mock lleva sus expectativas dentro del test: las reglas de una llamada válida se fijan antes de que el código se ejecute, y una llamada que las rompe falla en el acto. unittest.mock es más laxo, porque igual verificas después con un assert, pero create_autospec te da la mitad importante. Construye el double a partir de una clase real, así que cada llamada se comprueba contra las firmas reales de los métodos en el momento en que ocurre:
| |
Cambia place_order para que llame a self.gateway.charge(total, currency="usd") y este test falla con un TypeError dentro de place_order, porque Gateway.charge no acepta el argumento currency. Ahora imagina un Mock() normal en el mismo sitio, con alguien actualizando el assert para que cuadre con la nueva llamada. El test pasa, y el código se rompe la primera vez que se encuentra con la pasarela real. Un spy confirma que una llamada ocurrió. Un mock con autospec confirma además que esa llamada es una que el objeto real aceptaría.
Mock vs stub vs spy vs fake: tabla comparativa
| Double | Qué hace | Úsalo cuando |
|---|---|---|
| Dummy | Se pasa solo para satisfacer un parámetro; nunca se llama | Una dependencia existe solo porque la firma la exige en este camino |
| Stub | Devuelve siempre la misma respuesta fija, sea cual sea la entrada | El test necesita un valor fijo y determinista de una dependencia |
| Spy | Registra las llamadas para que el test pueda inspeccionarlas después | El test necesita confirmar que una interacción ocurrió, sin configurarla antes |
| Fake | Una implementación más pequeña pero funcional de la cosa real | El test necesita comportamiento real a través de varias llamadas, no un solo valor fijo |
| Mock | Se construye con expectativas sobre llamadas válidas, comprobadas en el momento en que ocurren | El test necesita verificar la forma exacta de una llamada: argumentos, número de veces, orden |
gateway fue un stub, un fake y un mock en tres tests de la misma clase. El double depende de qué comprueba el test, no de la dependencia.
Cuándo los test doubles empeoran los tests
Mockear cada colaborador acopla el test a cómo está escrito el código en lugar de a lo que hace. Añade un test que verifique que total_with_tax se llama exactamente una vez, y luego refactoriza place_order para que lo llame una segunda vez por motivos de logging: el test se rompe, y nada que le importe a un usuario ha cambiado. El test del fake de arriba no tiene ese problema, porque verifica el resultado (dos IDs de cobro distintos, los importes correctos registrados) en lugar de la forma de la llamada.
Mockear una librería que no es tuya tiene el mismo problema, un nivel más arriba. Haz autospec directamente de un cliente de terceros y cada test que lo toca depende de las firmas de método de esa librería; una actualización menor que renombra un parámetro rompe tests por toda la suite. Envuelve la librería en una pequeña interfaz propia y dobla esa interfaz en su lugar. Cuando la librería cambie, solo cambia el wrapper.
A veces el número correcto de doubles es cero. Si la dependencia real ya es rápida y no tiene efectos secundarios, como una función pura o una conexión SQLite en memoria, doblarla solo añade indirección y no detecta nada. Los doubles son para dependencias lentas, remotas, no deterministas, o con efectos secundarios que no quieres en una ejecución de tests.
Cómo elegir el test double correcto
Empieza por lo que comprueba el test, no por lo que parece más seguro de fakear. Si necesitas una entrada predecible para que un assert posterior se cumpla, usa un stub. Para confirmar que el SMS salió, usa un spy. Si la dependencia tiene que comportarse correctamente a través de varias llamadas, escribe un fake una vez y reutilízalo donde aparezca esa dependencia. Y cuando una llamada con argumentos incorrectos debería hacer fallar el test antes de que falle en producción, usa un mock con autospec.
Nada de esto funciona en una clase que construye sus propias dependencias internamente, porque no hay dónde meter un double. Ese es el problema que resuelve la dependency injection, y es lo primero que hay que arreglar si una clase se resiste a todos los doubles de este artículo. Una vez existe esa costura, decide qué double corresponde a qué nivel de test. Si trabajas test-first, vas a recurrir a los doubles pronto, porque el test existe antes de que haya código real al que llamar.