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 --coverage

Configure 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 vitest

Vitest detects changes and re-runs only affected tests. It's instant thanks to Vite.

Interactive UI

npx vitest --ui

Opens a web interface where you can:

  • See the status of all tests
  • Filter by file or name
  • View inline coverage
  • Re-run individual tests

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.