Por qué Vitest en lugar de Karma/Jasmine

Angular uso Karma + Jasmine durante más de una decada, pero en 2026 la comunidad ha migrado masivamente a Vitest por razones solidas:

  • Velocidad: Vitest es 5-10x más rápido que Karma gracias a Vite y ejecución en paralelo
  • DX moderna: Hot module replacement para tests, watch mode inteligente, UI interactiva
  • Ecosistema unificado: Mismo runner para unit tests, integration tests y coverage
  • Compatibilidad: Sintaxis compatible con Jest (describe, it, expect, vi.fn)
  • Sin navegador: Corre en Node con jsdom, eliminando la necesidad de levantar Chrome

Angular 21 ya soporta Vitest oficialmente a través del plugin de Analog.

Paso 1: Setup inicial

El primer bloque de código muestra como instalar y configurar Vitest en un proyecto Angular existente.

test-setup.ts

Crea el archivo de setup que inicializa el test environment de Angular:

// src/test-setup.ts
import '@analogjs/vitest-angular/setup-zone';

Si tu app es zoneless (usando provideExperimentalZonelessChangeDetection), usa en su lugar:

import '@analogjs/vitest-angular/setup-snapshots';

Scripts en package.json

Agrega los scripts necesarios:

{
  "scripts": {
    "test": "vitest",
    "test:run": "vitest run",
    "test:coverage": "vitest run --coverage",
    "test:ui": "vitest --ui"
  }
}

Paso 2: Testing de servicios

Los servicios son los más fáciles de testear porque generalmente son funciones puras o wrappers sobre APIs. El segundo bloque de código muestra como testear un SearchService.

Patron básico

describe('MiService', () => {
  let service: MiService;

  beforeEach(() => {
    TestBed.configureTestingModule({});
    service = TestBed.inject(MiService);
  });

  it('should do something', () => {
    const result = service.doSomething();
    expect(result).toBe(expectedValue);
  });
});

Servicios con dependencias

Cuando un servicio depende de otro, usa mocks:

describe('CourseService', () => {
  let service: CourseService;
  const mockFirestore = {
    getDoc: vi.fn(),
    setDoc: vi.fn(),
  };

  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [
        { provide: FirestoreService, useValue: mockFirestore },
      ],
    });
    service = TestBed.inject(CourseService);
  });

  it('should fetch course by slug', async () => {
    const mockCourse = { id: '1', slug: 'html-basics', title: 'HTML' };
    mockFirestore.getDoc.mockResolvedValue(mockCourse);

    const result = await service.getBySlug('html-basics');
    expect(result).toEqual(mockCourse);
    expect(mockFirestore.getDoc).toHaveBeenCalledWith(
      'courses',
      'html-basics'
    );
  });
});

Paso 3: Testing de componentes

Testear componentes Angular con Vitest es similar a como se hacia con Jasmine, pero con la sintaxis de Vitest. El tercer bloque de código muestra un ejemplo completo.

Puntos clave

  • Mocking signals: Crea signals reales para los mocks. Esto permite que el sistema de change detection funcione correctamente en los tests
  • vi.fn(): El equivalente a jasmine.createSpy() en Vitest
  • fixture.detectChanges(): Sigue siendo necesario para trigger change detection
  • nativeElement: Accede al DOM renderizado para assertions

Testing de inputs y outputs

Con la nueva API de signals para inputs y outputs:

import { ComponentFixture, TestBed } from '@angular/core/testing';
import { CardComponent } from './card.component';
import { describe, it, expect, beforeEach } from 'vitest';

describe('CardComponent', () => {
  let fixture: ComponentFixture<CardComponent>;

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      imports: [CardComponent],
    }).compileComponents();

    fixture = TestBed.createComponent(CardComponent);
  });

  it('should render title from input', () => {
    fixture.componentRef.setInput('title', 'Mi Título');
    fixture.detectChanges();

    const titleEl = fixture.nativeElement.querySelector('h3');
    expect(titleEl.textContent).toContain('Mi Título');
  });

  it('should emit on click', () => {
    const spy = vi.fn();
    fixture.componentInstance.clicked.subscribe(spy);

    const button = fixture.nativeElement.querySelector('button');
    button.click();

    expect(spy).toHaveBeenCalledOnce();
  });
});

Paso 4: Testing de signals

Angular signals requieren consideraciones especificas en tests:

import { signal, computed, effect } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { describe, it, expect } from 'vitest';

describe('Signal patterns', () => {
  it('should update computed when source changes', () => {
    const count = signal(0);
    const double = computed(() => count() * 2);

    expect(double()).toBe(0);

    count.set(5);
    expect(double()).toBe(10);
  });

  it('should update with complex objects', () => {
    interface CartItem {
      id: string;
      quantity: number;
    }

    const items = signal<CartItem[]>([]);
    const total = computed(() =>
      items().reduce((sum, item) => sum + item.quantity, 0)
    );

    items.update(current => [
      ...current,
      { id: 'item-1', quantity: 3 },
    ]);

    expect(total()).toBe(3);

    items.update(current => [
      ...current,
      { id: 'item-2', quantity: 2 },
    ]);

    expect(total()).toBe(5);
  });
});

Paso 5: Mocking avanzado

Mocking de módulos

import { vi } from 'vitest';

// Mock de un módulo completo
vi.mock('@angular/fire/firestore', () => ({
  Firestore: vi.fn(),
  collection: vi.fn(),
  doc: vi.fn(),
  getDoc: vi.fn(),
}));

Mocking de HTTP

Para servicios que hacen llamadas HTTP:

import { provideHttpClient } from '@angular/common/http';
import {
  HttpTestingController,
  provideHttpClientTesting,
} from '@angular/common/http/testing';

describe('ApiService', () => {
  let service: ApiService;
  let httpMock: HttpTestingController;

  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [
        provideHttpClient(),
        provideHttpClientTesting(),
      ],
    });
    service = TestBed.inject(ApiService);
    httpMock = TestBed.inject(HttpTestingController);
  });

  afterEach(() => {
    httpMock.verify();
  });

  it('should fetch data', () => {
    const mockData = [{ id: 1, name: 'Test' }];

    service.getData().subscribe(data => {
      expect(data).toEqual(mockData);
    });

    const req = httpMock.expectOne('/api/data');
    expect(req.request.method).toBe('GET');
    req.flush(mockData);
  });
});

Mocking de Router

import { provideRouter } from '@angular/router';
import { Router } from '@angular/router';

beforeEach(() => {
  TestBed.configureTestingModule({
    providers: [
      provideRouter([
        { path: 'cursos', component: DummyComponent },
      ]),
    ],
  });
});

Paso 6: Coverage

Vitest genera reportes de coverage con v8 (mucho más rápido que Istanbul):

npx vitest run --coverage

Configurar umbrales

En vitest.config.ts:

coverage: {
  provider: 'v8',
  thresholds: {
    statements: 80,
    branches: 75,
    functions: 80,
    lines: 80,
  },
},

Si los umbrales no se cumplen, el comando falla. Ideal para CI.

Paso 7: Watch mode y UI

Watch mode

npx vitest

Vitest detecta cambios y re-ejecuta solo los tests afectados. Es instantaneo gracias a Vite.

UI interactiva

npx vitest --ui

Abre una interfaz web donde puedes:

  • Ver el estado de todos los tests
  • Filtrar por archivo o nombre
  • Ver el coverage inline
  • Re-ejecutar tests individuales

Patrones recomendados

Arrange-Act-Assert

Estructura cada test con el patrón AAA:

it('should filter courses by category', () => {
  // Arrange
  const courses = [
    { id: '1', category: 'frontend' },
    { id: '2', category: 'backend' },
    { id: '3', category: 'frontend' },
  ];

  // Act
  const result = filterByCategory(courses, 'frontend');

  // Assert
  expect(result).toHaveLength(2);
  expect(result.every(c => c.category === 'frontend')).toBe(true);
});

Un assertion por test (cuando sea posible)

Tests con un solo assert son más fáciles de debuggear cuando fallan:

// Mejor
it('should return correct count', () => {
  expect(service.count()).toBe(5);
});

it('should return items in order', () => {
  expect(service.items()[0].id).toBe('first');
});

// Evitar
it('should return correct data', () => {
  expect(service.count()).toBe(5);
  expect(service.items()[0].id).toBe('first');
  expect(service.isLoading()).toBe(false);
  // Si falla en la linea 2, no sabemos nada de la linea 3
});

Nombra los tests como comportamientos

// Bien
it('should return empty array when no courses match the filter', () => {});
it('should navigate to course detail on card click', () => {});

// Mal
it('test filter', () => {});
it('click handler', () => {});

Conclusion

Vitest transforma la experiencia de testing en Angular. La velocidad, la DX moderna y la compatibilidad con el ecosistema lo hacen la opcion obvia para proyectos nuevos y una migración que vale la pena para proyectos existentes.

Empieza testeando tus servicios (son los más fáciles), luego avanza a componentes, y establece umbrales de coverage en CI para mantener la calidad a lo largo del tiempo. Tests rapidos que se ejecutan en cada commit son infinitamente más valiosos que tests lentos que nadie ejecuta.