Dobles de Test
Dummy, Stub, Spy, Mock y Fake no son intercambiables. Cada uno cumple un rol distinto, más allá de la librería que los crea.
Un doble de test (test double) es un reemplazo que se usa durante el testing para representar una dependencia real. Los términos más comunes —Dummy, Stub, Spy, Mock, Fake— no son intercambiables: cada uno cumple un rol distinto dentro del test, y esa distinción importa más que la herramienta usada para crearlos.
La taxonomía viene de xUnit Test Patterns, de Gerard Meszaros. Conviene no leerla como una escala de menor a mayor, porque los cinco no se ordenan en una sola línea: hay dos ejes independientes.
| No participa de la verificación | Participa de la verificación | |
|---|---|---|
| Sin comportamiento propio | Dummy | — |
| Devuelve respuestas fijas | Stub | Mock |
| Registra lo que le pasó | Spy | Spy + assertions |
| Implementación funcional | Fake | — |
El eje vertical es cuánta implementación real tiene. El eje horizontal es si el doble forma parte de lo que el test verifica. Un Mock no tiene más comportamiento real que un Stub: es un Stub que además exige que se lo haya llamado de cierta forma.
Esa distinción se traduce en una diferencia práctica que sí importa:
- Verificación de estado — el test mira el resultado o el estado final del sistema. Usa Dummy, Stub o Fake.
- Verificación de interacción — el test mira cómo la unidad usó a su colaborador. Usa Spy o Mock.
Dummy
Se usa porque una dependencia tiene que estar presente, pero el test no interactúa con ella:
const logger = new DummyLogger();
const service = new ReservationService(reservationRepository, logger);
Stub
Devuelve respuestas predefinidas. Si ReservationService consulta a PricingService para saber cuánto cobrar de seña, el test controla esa respuesta y después verifica el resultado:
const pricingStub: PricingService = {
getDepositAmount: () => Money.of(2000),
};
const service = new ReservationService(pricingStub, tableRepository);
const reservation = service.create(request);
// La assertion es sobre el resultado, no sobre el stub
expect(reservation.deposit).toEqual(Money.of(2000));
La pregunta detrás de un stub es: ¿qué respuesta necesito que entregue esta dependencia para llegar al caso que quiero verificar? El foco no está en cómo fue usada, sino en el estado final.
Spy
Registra lo que le pasó, para que el test lo inspeccione después de la acción. Si ReservationService avisa a NotificationService cuando una reserva se confirma, el spy anota la llamada y el test la revisa al final:
class NotificationSpy implements NotificationService {
readonly sent: ReservationId[] = [];
sendConfirmation(id: ReservationId): void {
this.sent.push(id); // solo registra, no juzga
}
}
// Act
const spy = new NotificationSpy();
new ReservationService(pricingStub, spy).confirm(reservationId);
// Assert: la verificación vive en el test, al final
expect(spy.sent).toEqual([reservationId]);
El spy es pasivo: no sabe qué se espera de él. Todo el criterio está en el bloque de assertions.
Mock
Un mock lleva la expectativa adentro: se le dice de antemano qué llamadas debe recibir, y él mismo falla si no ocurren como se pactó.
const paymentMock = mock<PaymentGateway>();
// La expectativa se declara ANTES de ejecutar
paymentMock.expect('charge').once().with(Money.of(2000));
new ReservationService(pricingStub, paymentMock).confirm(reservationId);
// No hay assertion sobre valores: se le pide al mock que se verifique
paymentMock.verify();
Los mocks se justifican cuando la interacción es el comportamiento: que se cobre exactamente una vez, que se publique el evento, que no se llame al proveedor si la reserva ya estaba cancelada. Para todo lo demás, verificar el resultado envejece mucho mejor.
Fake
Una implementación simplificada pero funcional de una dependencia. A diferencia de un stub, un fake tiene comportamiento propio:
const repository = new InMemoryReservationRepository();
repository.save(reservation);
const result = repository.findById(reservation.id);
Un InMemoryReservationRepository puede ser mucho más simple que Postgres, pero sigue siendo una representación suficientemente realista para determinado tipo de test.
| Test Double | Propósito principal |
|---|---|
| Dummy | Satisfacer una dependencia que no se usa |
| Stub | Dar respuestas controladas |
| Spy | Registrar interacciones |
| Mock | Verificar interacciones esperadas, fallando por sí mismo |
| Fake | Implementación simplificada pero funcional |
La regla que resume el artículo: elegí el doble por lo que el test necesita verificar, no por la dependencia que estás reemplazando. El mismo PaymentService puede ser un dummy en un test de validación, un stub en uno de cálculo y un mock en uno que protege que no se cobre dos veces.
Que reemplazar una dependencia sea fácil no es casualidad: depende de que la unidad reciba una abstracción en lugar de construir directamente una implementación concreta. Cuando aislar una dependencia resulta difícil, suele ser una señal de que el diseño tiene un acoplamiento innecesario. Es una de las ideas centrales de la Inversión de Dependencias.
La combinación de aislamiento, AAA y dobles de test permite construir unit tests claros y mantenibles. Sobre estas mismas ideas se apoya Test-Driven Development: usar los tests como parte del proceso de diseño, no solo como verificación posterior.
