Why Vitest instead of Karma/Jasmine
Angular used Karma + Jasmine for over a decade, but in 2026 the community has massively migrated to Vitest for solid reasons:
- Speed: Vitest is 5-10x faster than Karma thanks to Vite and parallel execution
- Modern DX: Hot module replacement for tests, smart watch mode, interactive UI
- Unified ecosystem: Same runner for unit tests, integration tests, and coverage
- Compatibility: Jest-compatible syntax (describe, it, expect, vi.fn)
- No browser: Runs in Node with jsdom, eliminating the need to launch Chrome
Angular 21 already officially supports Vitest through the Analog plugin.
Step 1: Initial setup
The first code block shows how to install and configure Vitest in an existing Angular project.
test-setup.ts
Create the setup file that initializes the Angular test environment:
// src/test-setup.ts
import '@analogjs/vitest-angular/setup-zone';If your app is zoneless (using provideExperimentalZonelessChangeDetection), use instead:
import '@analogjs/vitest-angular/setup-snapshots';Scripts in package.json
Add the necessary scripts:
{
"scripts": {
"test": "vitest",
"test:run": "vitest run",
"test:coverage": "vitest run --coverage",
"test:ui": "vitest --ui"
}
}Step 2: Testing services
Services are the easiest to test because they are generally pure functions or wrappers over APIs. The second code block shows how to test a SearchService.
Basic pattern
describe('MiService', () => {
let service: MiService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(MiService);
});
it('should do something', () => {
const result = service.doSomething();
expect(result).toBe(expectedValue);
});
});Services with dependencies
When a service depends on another, use 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'
);
});
});Step 3: Testing components
Testing Angular components with Vitest is similar to how it was done with Jasmine, but with Vitest syntax. The third code block shows a complete example.
Key points
- Mocking signals: Create real signals for mocks. This allows the change detection system to work correctly in tests
- vi.fn(): The Vitest equivalent of jasmine.createSpy()
- fixture.detectChanges(): Still necessary to trigger change detection
- nativeElement: Access the rendered DOM for assertions
Testing inputs and outputs
With the new signals API for inputs and 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();
});
});Step 4: Testing signals
Angular signals require specific considerations in 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);
});
});Step 5: Advanced mocking
Module mocking
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(),
}));HTTP mocking
For services that make HTTP calls:
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);
});
});Router mocking
import { provideRouter } from '@angular/router';
import { Router } from '@angular/router';
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
provideRouter([
{ path: 'cursos', component: DummyComponent },
]),
],
});
});Step 6: Coverage
Vitest generates coverage reports with v8 (much faster than Istanbul):
npx vitest run --coverageConfigure thresholds
In vitest.config.ts:
coverage: {
provider: 'v8',
thresholds: {
statements: 80,
branches: 75,
functions: 80,
lines: 80,
},
},If the thresholds are not met, the command fails. Ideal for CI.
Step 7: Watch mode and UI
Watch mode
npx vitestVitest detects changes and re-runs only affected tests. It's instant thanks to Vite.
Interactive UI
npx vitest --uiOpens a web interface where you can:
- See the status of all tests
- Filter by file or name
- View inline coverage
- Re-run individual tests
Recommended patterns
Arrange-Act-Assert
Structure each test with the AAA pattern:
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);
});One assertion per test (when possible)
Tests with a single assert are easier to debug when they fail:
// Better
it('should return correct count', () => {
expect(service.count()).toBe(5);
});
it('should return items in order', () => {
expect(service.items()[0].id).toBe('first');
});
// Avoid
it('should return correct data', () => {
expect(service.count()).toBe(5);
expect(service.items()[0].id).toBe('first');
expect(service.isLoading()).toBe(false);
// If line 2 fails, we know nothing about line 3
});Name tests as behaviors
// Good
it('should return empty array when no courses match the filter', () => {});
it('should navigate to course detail on card click', () => {});
// Bad
it('test filter', () => {});
it('click handler', () => {});Conclusion
Vitest transforms the testing experience in Angular. The speed, modern DX, and ecosystem compatibility make it the obvious choice for new projects and a worthwhile migration for existing ones.
Start by testing your services (they're the easiest), then move on to components, and set coverage thresholds in CI to maintain quality over time. Fast tests that run on every commit are infinitely more valuable than slow tests that nobody runs.



Comments (0)
Sign in to comment