DEV Community

Cover image for Testing Angular 21 Components with Vitest: A Complete Guide
Olayinka Akeju
Olayinka Akeju

Posted on

Testing Angular 21 Components with Vitest: A Complete Guide

Angular 21 replaced Karma with Vitest as the default testing framework. If you're used to Karma or Jest, this shift means new syntax, different patterns, and a fresh approach to testing, especially with Angular's signals and new control flow.

What You'll Learn

  • Setting up Vitest in Angular 21
  • Testing standalone components with signals and computed signals
  • Working with Angular's control flow syntax (@for, @if, @empty)
  • Understanding fixture.detectChanges() for DOM updates
  • Testing user interactions and edge cases

We're testing a full-featured task manager: add tasks, mark them complete, filter by status, and delete them. All code is on GitHub.

Project Setup

Angular 21 includes Vitest out of the box. Create a new project and you're ready to go:

ng new angular-vitest-testing-guide
cd angular-vitest-testing-guide
Enter fullscreen mode Exit fullscreen mode

Verify Vitest is Installed

Check package.json:

{
  "devDependencies": {
    "@angular/build": "^21.0.0",
    "vitest": "^4.0.8"
  }
}
Enter fullscreen mode Exit fullscreen mode

Check angular.json:

{
  "projects": {
    "your-project-name": {
      "architect": {
        "test": {
          "builder": "@angular/build:unit-test"
        }
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Run Tests

# Watch mode (default)
ng test

# Single run for CI
ng test --no-watch
Enter fullscreen mode Exit fullscreen mode

Output should look like:

✓  angular-vitest-testing-guide  src/app/app.spec.ts (2 tests) 130ms
   ✓ App (2)
     ✓ should create the app 93ms
     ✓ should render title 35ms

 Test Files  1 passed (1)
      Tests  2 passed (2)
Enter fullscreen mode Exit fullscreen mode

Optional: Code Coverage

Coverage needs an extra package:

npm install @vitest/coverage-v8 --save-dev
ng test --coverage
Enter fullscreen mode Exit fullscreen mode

Coverage reports land in the coverage/ directory.

The Component We're Testing

The TaskList component handles everything you'd expect in a real task manager: adding tasks with validation, marking complete, filtering, and deleting. This mirrors production scenarios.

Features:

  • Add tasks (rejects empty/whitespace-only input)
  • Toggle completion status
  • Delete tasks
  • Filter by status (all, active, completed)
  • Clear all completed tasks
  • Show task counts
  • Display filter-specific empty states

Full component code on GitHub

Core implementation:

export class TaskListComponent {
  tasks = signal<Task[]>([]);
  filter = signal<TaskFilter>('all');

  // Computed signals auto-update when dependencies change
  activeTasksCount = computed(() =>
    this.tasks().filter(t => !t.completed).length
  );

  filteredTasks = computed(() => {
    switch (this.filter()) {
      case 'active': return this.tasks().filter(t => !t.completed);
      case 'completed': return this.tasks().filter(t => t.completed);
      default: return this.tasks();
    }
  });

  addTask() {
    const text = this.newTaskText().trim();
    if (!text) return;

    this.tasks.update(tasks => [...tasks, {
      id: Date.now(),
      text,
      completed: false,
      createdAt: new Date()
    }]);
    this.newTaskText.set('');
  }
}
Enter fullscreen mode Exit fullscreen mode

Writing Your First Tests

Generate the component with its test file:

ng g c features/week-01-basics/task-list/components/task-list --type=component
Enter fullscreen mode Exit fullscreen mode

Important: The --type=component flag adds the .component suffix and keeps Component in the class name. Without it, you get task-list.ts instead of task-list.component.ts, and TaskList instead of TaskListComponent.

Test File Structure

Basic Vitest setup for Angular components:

import { ComponentFixture, TestBed } from '@angular/core/testing';
import { TaskListComponent } from './task-list.component';

describe('TaskListComponent', () => {
  let component: TaskListComponent;
  let fixture: ComponentFixture<TaskListComponent>;
  let compiled: HTMLElement;

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      imports: [TaskListComponent] // Standalone = imports, not declarations
    }).compileComponents();

    fixture = TestBed.createComponent(TaskListComponent);
    component = fixture.componentInstance;
    compiled = fixture.nativeElement as HTMLElement;
    fixture.detectChanges();
  });

  it('should create', () => {
    expect(component).toBeTruthy();
  });
});
Enter fullscreen mode Exit fullscreen mode

Key pieces:

  • ComponentFixture - Wrapper with testing utilities
  • component - Direct access to the TypeScript class
  • compiled - The actual rendered HTML
  • fixture.detectChanges() - Triggers change detection (more on this soon)

Testing Component Initialization

Verify the component starts in the correct state:

describe('Component Setup', () => {
  it('should create the component', () => {
    expect(component).toBeTruthy();
  });

  it('should initialize with empty tasks array', () => {
    expect(component.tasks()).toEqual([]);
  });

  it('should initialize with empty input text', () => {
    expect(component.newTaskText()).toBe('');
  });

  it('should initialize with "all" filter', () => {
    expect(component.filter()).toBe('all');
  });

  it('should display empty state message initially', () => {
    const emptyState = compiled.querySelector('[data-testid="empty-state"]');
    expect(emptyState).toBeTruthy();
    expect(emptyState?.textContent).toContain('No tasks yet');
  });
});
Enter fullscreen mode Exit fullscreen mode

Testing Signals

Signals are functions, you call them to read, use .set() or .update() to write:

// Read a signal value
expect(component.tasks()).toEqual([]);

// Signals are just functions with ()
const currentTasks = component.tasks();
const currentFilter = component.filter();
Enter fullscreen mode Exit fullscreen mode

Much simpler than Observables with their subscriptions and async complexity.

Testing User Interactions

Test the main feature: adding tasks.

describe('Adding Tasks', () => {
  it('should add a task with valid text', () => {
    // ARRANGE - Set up test data
    component.newTaskText.set('Go to the store');

    // ACT - Perform the action
    component.addTask();

    // ASSERT - Verify the result
    expect(component.tasks()).toHaveLength(1);
    expect(component.tasks()[0].text).toBe('Go to the store');
    expect(component.tasks()[0].completed).toBe(false);
  });

  it('should clear input after adding task', () => {
    component.newTaskText.set('Go to the store