Mocks vs. stubs vs. fakes vs. spies: ¿qué es realmente distinto, y cuándo usás cada uno?
Los cuatro tipos de test double se dividen claramente en dos familias — los que controlan la entrada y los que verifican interacciones — y confundirlos es la causa más común de tests frágiles.
APRENDE HACIENDOPon a prueba tu intuición3 pasos
Mocks vs. stubs vs. fakes vs. spies: ¿qué es realmente distinto, y cuándo usás cada uno?
Usa los ejemplos y la explicación de la lección para comprobar tu idea.
Reflexión personal. No se envía a ningún servicio de IA.#Por Qué el Vocabulario Realmente Importa
"Mockealo y ya" es la frase que más confusión genera en este tema, porque en el uso informal "mock" se usa para cualquier tipo de test double. La distinción importa porque cada tipo responde una pregunta distinta, y usar el equivocado es una fuente común y concreta de tests frágiles o engañosos. La taxonomía clásica — acuñada por Gerard Meszaros y popularizada después por Martin Fowler — traza una línea firme entre los dobles que te dejan controlar la entrada (stubs, fakes) y los que te dejan verificar interacciones (mocks, spies).
#Cuatro Tipos, Una Sola Dependencia
Los cuatro ejemplos de abajo duplican la misma dependencia: un PaymentGateway usado por el método OrderService.checkout().
interface PaymentGateway {charge(amount: number, cardToken: string): Promise<{ id: string; status: 'succeeded' | 'declined' }>;}
#Stub — Devuelve una Respuesta Enlatada, Sin Verificación de Comportamiento
Un stub existe únicamente para hacer posible el test devolviendo un valor fijo; nunca afirmás nada sobre cómo fue llamado.
test('checkout marca la orden como PAID cuando el gateway responde con éxito (stub)', async () => {const stubGateway: PaymentGateway = {charge: async () => ({ id: 'ch_1', status: 'succeeded' }),};const orderService = new OrderService(stubGateway);const result = await orderService.checkout('order-1', 4999, 'tok_visa');expect(result.status).toBe('PAID');// Nunca afirmamos *cómo* se llamó a charge -- el stub solo entrega una respuesta enlatada.});
#Mock — Programado de Antemano con Expectativas Sobre Cómo Será Llamado
Un mock se usa específicamente para verificar una interacción — que una llamada ocurrió, con qué argumentos, cuántas veces.
test('checkout cobra exactamente el total de la orden, una sola vez (mock)', async () => {const mockCharge = jest.fn().mockResolvedValue({ id: 'ch_2', status: 'succeeded' });const mockGateway: PaymentGateway = { charge: mockCharge };const orderService = new OrderService(mockGateway);await orderService.checkout('order-2', 4999, 'tok_visa');expect(mockCharge).toHaveBeenCalledTimes(1);expect(mockCharge).toHaveBeenCalledWith(4999, 'tok_visa');});
#Fake — Una Implementación Real y Funcional, No Apta Para Producción
Un fake no es una respuesta enlatada — es lógica real que funciona, normalmente una versión simplificada en memoria de la cosa real.
class InMemoryPaymentGateway implements PaymentGateway {private ledger: { id: string; amount: number; cardToken: string }[] = [];private nextId = 1;async charge(amount: number, cardToken: string) {if (cardToken === 'tok_chargeDeclined') {return { id: `ch_${this.nextId++}`, status: 'declined' as const };}const id = `ch_${this.nextId++}`;this.ledger.push({ id, amount, cardToken });return { id, status: 'succeeded' as const };}totalCharged(): number {return this.ledger.reduce((sum, entry) => sum + entry.amount, 0);}}test('dos checkouts exitosos acumulan el total correcto en el ledger (fake)', async () => {const fakeGateway = new InMemoryPaymentGateway();const orderService = new OrderService(fakeGateway);await orderService.checkout('order-3', 4999, 'tok_visa');await orderService.checkout('order-4', 1500, 'tok_visa');expect(fakeGateway.totalCharged()).toBe(6499);});test('una tarjeta rechazada deja el ledger intacto (fake)', async () => {const fakeGateway = new InMemoryPaymentGateway();const orderService = new OrderService(fakeGateway);await expect(orderService.checkout('order-5', 2000, 'tok_chargeDeclined')).rejects.toThrow('payment declined');expect(fakeGateway.totalCharged()).toBe(0);});
Fijate que acá el fake se gana su lugar de una forma que ni el stub ni el mock pueden: tiene comportamiento real y con estado, así que puede expresar escenarios como "un segundo cobro debería ver el efecto del primero" sin tener que codificar a mano cada valor de retorno posible dentro del test.
#Spy — Envuelve un Objeto Real y Registra Cómo se lo Usó
Un spy es primo del mock: también te deja afirmar sobre las llamadas, pero envuelve una implementación real en lugar de reemplazarla, así que el comportamiento real sigue corriendo.
test('checkout invoca al gateway real con los argumentos correctos (spy)', async () => {const realGateway = new InMemoryPaymentGateway();const chargeSpy = jest.spyOn(realGateway, 'charge');const orderService = new OrderService(realGateway);await orderService.checkout('order-6', 2000, 'tok_visa');expect(chargeSpy).toHaveBeenCalledWith(2000, 'tok_visa');expect(realGateway.totalCharged()).toBeGreaterThan(0); // la lógica real siguió corriendo});
#La Tabla de Decisión
| Tipo | ¿Controla el valor de retorno? | ¿Verifica interacciones? | ¿Corre lógica real? | Usalo cuando... |
|---|---|---|---|---|
| Stub | Sí, fijo | No | No | Solo necesitás que una dependencia devuelva algo para que el código bajo test pueda avanzar |
| Mock | Sí, fijo | Sí | No | El hecho de que ocurrió una llamada, con argumentos específicos, es lo que estás probando |
| Fake | No — se calcula | No (usualmente) | Sí, simplificada | Necesitás comportamiento con estado y realista a través de múltiples llamadas, sin una dependencia real de infraestructura |
| Spy | No — real | Sí | Sí | Querés afirmar cómo se usó un objeto real sin cambiar su comportamiento |
#La Trampa: Sobre-Mockear Acopla los Tests a la Implementación
🧠 Check your understanding
0/2 · 0/2 answered1. Querés afirmar que OrderService llamó al enviador de emails exactamente una vez con la dirección del cliente, y no hay ningún otro efecto secundario observable para chequear. ¿En qué tipo de test double estás confiando?
2. ¿Qué tipo de test double es más probable que vuelva frágil una suite si se abusa de él, porque los tests terminan acoplados a detalles de implementación en vez de a resultados?