Cómo y cuándo usar Mocks vs Spies en pruebas unitarias (Vitest, Jest y Jasmine)

Cuando escribo pruebas unitarias para servicios de Angular o clases de dominio en TypeScript, rara vez pruebo funciones en aislamiento total. Nuestras clases tienen dependencias: clientes HTTP, proveedores de autenticación, calculadoras de impuestos o SDKs de terceros.
Para evitar que las pruebas hagan llamadas a servidores reales o fallen cuando cambie una lógica secundaria, usamos Test Doubles. Dos de los más comunes son los Mocks y los Spies.
⚡ ¿Usas Angular moderno con Vitest o Jest? Salta directo a Test runners modernos: Mocking y Spying en Vitest y Jest para ver cómo usar
vi.fn()yvi.spyOn().
En esta guía te voy a explicar la diferencia entre Mocks y Spies, veremos escenarios prácticos y revisaremos ejemplos en Vitest, Jest y Jasmine.
Empecemos definiendo nuestro escenario de prueba.
1. El escenario: Servicio de procesamiento de facturas 🧾
Imagina un InvoiceService que calcula el total de una factura según reglas de impuestos y envía la factura finalizada a una API externa del gobierno:
// invoice.service.ts
import { Injectable, inject } from '@angular/core';
export class TaxCalculationService {
getTaxRate(): number {
// En la vida real, esto podría hacer una llamada HTTP a un servicio tributario
return 0.18;
}
}
export class GovernmentExportService {
sendToGovernment(invoiceId: number): boolean {
console.log(`Sending invoice ${invoiceId} to government gateway...`);
return true;
}
}
export class InvoiceService {
id: number = Math.floor(Math.random() * 1000000);
isProcessed: boolean = false;
constructor(
private taxService: TaxCalculationService,
private exportService: GovernmentExportService
) {}
calculateTotal(subtotal: number): number {
const rate = this.taxService.getTaxRate();
return subtotal + (subtotal * rate);
}
processInvoice(): boolean {
const success = this.exportService.sendToGovernment(this.id);
if (success) {
this.isProcessed = true;
}
return this.isProcessed;
}
}El problema de probar con instancias reales
Si probamos InvoiceService instanciando los servicios reales TaxCalculationService y GovernmentExportService:
- Si la lógica del cálculo de impuestos cambia, nuestras pruebas de facturación se rompen (pruebas frágiles).
GovernmentExportServicepodría intentar realizar llamadas de red reales o efectos secundarios durante la ejecución de las pruebas.
Veamos cómo los Mocks resuelven el primer problema.
2. Uso de Mocks: Controlando valores de retorno 🎭
Un Mock reemplaza una dependencia por un objeto simulado bajo nuestro control. Tú decides qué valores retorna para verificar cómo reacciona tu unidad ante salidas específicas.
Cuándo usar un Mock:
- Cuando quieres aislar tu clase de cálculos externos o respuestas HTTP.
- Cuando necesitas probar casos límite (por ejemplo, tasa de impuesto en
0, una API que responde404o un servicio que lanza un error).
Ejemplo con Jasmine (jasmine.createSpyObj)
import { InvoiceService, TaxCalculationService, GovernmentExportService } from './invoice.service';
describe('InvoiceService with Mocks (Jasmine)', () => {
let invoiceService: InvoiceService;
let mockTaxService: jasmine.SpyObj<TaxCalculationService>;
let mockExportService: jasmine.SpyObj<GovernmentExportService>;
beforeEach(() => {
// 1. Crear objetos mock con los métodos designados
mockTaxService = jasmine.createSpyObj<TaxCalculationService>(['getTaxRate']);
mockExportService = jasmine.createSpyObj<GovernmentExportService>(['sendToGovernment']);
// 2. Controlar los valores de retorno
mockTaxService.getTaxRate.and.returnValue(0.20); // 20% de impuesto
// 3. Inyectar los mocks en el servicio
invoiceService = new InvoiceService(mockTaxService, mockExportService);
});
it('should calculate total with mocked tax rate', () => {
const total = invoiceService.calculateTotal(100);
expect(total).toBe(120); // 100 + (100 * 0.20)
});
});Ahora veamos en qué se diferencian los Spies cuando solo necesitas observar la ejecución de una función.
3. Uso de Spies: Verificando ejecuciones y argumentos 🕵️♂️
Un Spy monitorea la ejecución de funciones. Su trabajo es observar:
- ¿Se llamó al método?
- ¿Cuántas veces se ejecutó?
- ¿Qué argumentos exactos recibió?
Cuándo usar un Spy:
- Cuando no necesitas reemplazar el dato devuelto, pero sí quieres verificar que una acción secundaria (como registrar logs, enviar analíticas o persistir datos en almacenamiento) ocurrió con los parámetros correctos.
Ejemplo con Jasmine (spyOn)
it('should verify sendToGovernment was called with invoice ID', () => {
mockExportService.sendToGovernment.and.returnValue(true);
const result = invoiceService.processInvoice();
// Verificar la ejecución y los argumentos exactos
expect(mockExportService.sendToGovernment).toHaveBeenCalledTimes(1);
expect(mockExportService.sendToGovernment).toHaveBeenCalledWith(invoiceService.id);
expect(result).toBe(true);
});Ahora pasemos a revisar las pruebas modernas en Angular con Vitest.
4. Test runners modernos: Mocking y Spying en Vitest y Jest ⚡
Angular moderno (v18 y v19+) ofrece soporte de primer nivel para Vitest (mediante @angular/build y las herramientas de Vite) y Jest, dejando atrás la configuración obsoleta de Karma y Protractor.
Tanto en Vitest como en Jest, la API es concisa, unificada y estándar en todo el ecosistema de TypeScript.
Mocking y Spying con Vitest (vi.fn y vi.spyOn)
// invoice.service.spec.ts (Vitest)
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { InvoiceService, TaxCalculationService, GovernmentExportService } from './invoice.service';
describe('InvoiceService (Vitest)', () => {
let invoiceService: InvoiceService;
let mockTaxService: Partial<TaxCalculationService>;
let mockExportService: Partial<GovernmentExportService>;
beforeEach(() => {
// 1. Crear el mock con vi.fn()
mockTaxService = {
getTaxRate: vi.fn().mockReturnValue(0.10), // 10% de impuesto
};
mockExportService = {
sendToGovernment: vi.fn().mockReturnValue(true),
};
invoiceService = new InvoiceService(
mockTaxService as TaxCalculationService,
mockExportService as GovernmentExportService
);
});
it('should calculate total with Vitest mock', () => {
const total = invoiceService.calculateTotal(200);
expect(total).toBe(220);
expect(mockTaxService.getTaxRate).toHaveBeenCalledOnce();
});
it('should spy on government export execution', () => {
invoiceService.processInvoice();
expect(mockExportService.sendToGovernment).toHaveBeenCalledWith(invoiceService.id);
expect(invoiceService.isProcessed).toBe(true);
});
});Guía rápida de conversión de sintaxis
| Acción | Jasmine (Karma) | Jest | Vitest (Angular moderno) |
|---|---|---|---|
| Crear función mock | jasmine.createSpy() | jest.fn() | vi.fn() |
| Simular valor de retorno | .and.returnValue(val) | .mockReturnValue(val) | .mockReturnValue(val) |
| Espiar un método | spyOn(obj, 'method') | jest.spyOn(obj, 'method') | vi.spyOn(obj, 'method') |
| Verificar cantidad de llamadas | expect(s).toHaveBeenCalledTimes(1) | expect(s).toHaveBeenCalledTimes(1) | expect(s).toHaveBeenCalledOnce() |
Comparación resumida: Mock vs Spy 📊
| Característica | Mock 🎭 | Spy 🕵️♂️ |
|---|---|---|
| Objetivo principal | Reemplazar la dependencia y controlar la salida devuelta | Observar y registrar las invocaciones del método |
| Implementación interna | Objeto simulado o preparado con datos fijos | Envuelve el método existente o registra llamadas |
| Enfoque de verificación | Verificación de estado (¿La clase calculó el resultado correcto?) | Verificación de comportamiento (¿La clase llamó al colaborador?) |
| Mejor escenario | APIs HTTP, cálculos complejos, llamadas a bases de datos | Registro de logs, disparo de eventos, notificaciones |
Resumen 🛠️
Para escribir pruebas unitarias limpias y resistentes:
- Usa Mocks (
vi.fn()/jest.fn()) para simular respuestas de servicios externos y evaluar casos límite. - Usa Spies (
vi.spyOn()) para verificar que los métodos se invoquen con los argumentos correctos. - En proyectos modernos de Angular, adopta Vitest para disfrutar de una ejecución rápida y con soporte nativo de ESM.
Si quieres más guías sobre pruebas en Angular, revisa mis artículos sobre Pruebas de componentes en Angular con ng-mocks y Cómo probar componentes usando TestBed.
These are my experiences learning and facing my daily challenges working with Angular.
Preguntas Frecuentes
¿Cuál es la diferencia principal entre un Mock y un Spy en pruebas unitarias?
Un Mock reemplaza una dependencia real por un objeto simulado que devuelve respuestas predefinidas, aislando la unidad bajo prueba. Un Spy envuelve un objeto o método existente para observar, registrar argumentos y verificar su ejecución sin reemplazar necesariamente la lógica interna.
¿Cómo creo mocks y spies en Angular moderno con Vitest?
En Vitest (el test runner moderno en Angular), crea funciones mock usando 'vi.fn().mockReturnValue(val)' y crea spies de métodos usando 'vi.spyOn(object, 'methodName')'.
¿Cuándo debo usar un Mock en lugar de un Spy?
Usa un Mock cuando la dependencia real realice llamadas HTTP externas, acceda a una base de datos o haga cálculos pesados cuya salida quieras controlar. Usa un Spy cuando quieras verificar que un método fue ejecutado con argumentos específicos.
¿Cómo se comparan los test runners en Angular entre Jasmine, Jest y Vitest?
Jasmine/Karma es el test runner tradicional en proyectos antiguos de Angular. Angular moderno (v18 y v19+) apuesta por pruebas basadas en Vite con Vitest o Jest, ofreciendo ejecución instantánea, soporte nativo para ESM y mocking simplificado con vi.fn() y vi.spyOn().
Artículos Relacionados
Pruebas de componentes en Angular: NO_ERRORS_SCHEMA, componentes stub y ng-mocks
Aprende cómo probar componentes de Angular con dependencias hijas complejas. Compara NO_ERRORS_SCHEMA, componentes stub manuales y ng-mocks, tanto en Angular tradicional como en Standalone moderno.
Cómo probar componentes en Angular usando TestBed
Aprende a probar componentes en Angular con TestBed, manejar componentes hijos y mocks de servicios, y migrar de Karma/Jasmine al moderno Vitest con Standalone y Signals.
Cómo configurar Jest en Angular 18
Al escribir pruebas en Angular, Jasmine y Karma vienen configurados por defecto como librería y test runner, pero el equipo de Angular ha estado trabajando para traer Jest. Hoy vamos a experimentar con el soporte experimental de Jest Builder en Angular.
Compartir este artículo
Si esta guía te resultó útil, compártela con tu equipo o con otros desarrolladores.
Software real. Lecciones reales.
Comparto las lecciones que aprendí por las malas, para que puedas evitarlas o estar preparado cuando ocurran.
Únete a 13,800+ desarrolladores y lectores.
Cero spam. Cancela tu suscripción en cualquier momento.