🇺🇸 EN
Angular
·6 min read·↗·🇺🇸 EN

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

Portada principal del artículo: 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() y vi.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:

  1. Si la lógica del cálculo de impuestos cambia, nuestras pruebas de facturación se rompen (pruebas frágiles).
  2. GovernmentExportService podrí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 responde 404 o 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ónJasmine (Karma)JestVitest (Angular moderno)
Crear función mockjasmine.createSpy()jest.fn()vi.fn()
Simular valor de retorno.and.returnValue(val).mockReturnValue(val).mockReturnValue(val)
Espiar un métodospyOn(obj, 'method')jest.spyOn(obj, 'method')vi.spyOn(obj, 'method')
Verificar cantidad de llamadasexpect(s).toHaveBeenCalledTimes(1)expect(s).toHaveBeenCalledTimes(1)expect(s).toHaveBeenCalledOnce()

Comparación resumida: Mock vs Spy 📊

CaracterísticaMock 🎭Spy 🕵️‍♂️
Objetivo principalReemplazar la dependencia y controlar la salida devueltaObservar y registrar las invocaciones del método
Implementación internaObjeto simulado o preparado con datos fijosEnvuelve el método existente o registra llamadas
Enfoque de verificaciónVerificación de estado (¿La clase calculó el resultado correcto?)Verificación de comportamiento (¿La clase llamó al colaborador?)
Mejor escenarioAPIs HTTP, cálculos complejos, llamadas a bases de datosRegistro de logs, disparo de eventos, notificaciones

Resumen 🛠️

Para escribir pruebas unitarias limpias y resistentes:

  1. Usa Mocks (vi.fn() / jest.fn()) para simular respuestas de servicios externos y evaluar casos límite.
  2. Usa Spies (vi.spyOn()) para verificar que los métodos se invoquen con los argumentos correctos.
  3. 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.

Parte de la serie Angular

These are my experiences learning and facing my daily challenges working with Angular.

Ver serie completa

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

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.

User avatar
User avatar
User avatar
User avatar
+13K

Únete a 13,800+ desarrolladores y lectores.

Cero spam. Cancela tu suscripción en cualquier momento.