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 --coverageConfigurar 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 vitestVitest detecta cambios y re-ejecuta solo los tests afectados. Es instantaneo gracias a Vite.
UI interactiva
npx vitest --uiAbre 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.



Comentarios (0)
Inicia sesión para comentar