On this page
Basic testing with Vitest
Testing in Angular
Automated testing verifies that your code works correctly and prevents regressions. Angular 21 supports Vitest as a modern and fast test runner.
Types of tests
| Type | What it tests | Speed | Example |
|---|---|---|---|
| Unit | An isolated function/class | Very fast | Services, pipes, utils |
| Component | A component with template | Fast | Rendering, interaction |
| Integration | Multiple components together | Medium | User flows |
| E2E | The complete app in a browser | Slow | Registration, login, purchase |
Configuring Vitest
Angular 21 can use Vitest instead of Karma/Jasmine:
npm install -D vitest @analogjs/vitest-angular jsdomConfiguration in vitest.config.ts:
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true,
environment: 'jsdom',
include: ['src/**/*.spec.ts'],
setupFiles: ['src/test-setup.ts'],
},
});TestBed
TestBed is Angular's testing environment. It configures a testing module with the necessary providers:
import { TestBed } from '@angular/core/testing';
beforeEach(() => {
TestBed.configureTestingModule({
providers: [MyService],
});
});Testing services
Services are the easiest to test because they are pure classes:
describe('CalculatorService', () => {
let service: CalculatorService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(CalculatorService);
});
it('should add two numbers', () => {
expect(service.add(2, 3)).toBe(5);
});
it('should handle negative numbers', () => {
expect(service.add(-1, 5)).toBe(4);
});
});Testing signals
Signals are tested by reading their value directly:
it('should update the signal', () => {
const service = TestBed.inject(CounterService);
expect(service.value()).toBe(0);
service.increment();
expect(service.value()).toBe(1);
// Computed signals too
expect(service.isEven()).toBe(false);
});Testing pipes
Pipes are pure functions, ideal for testing:
import { TruncatePipe } from './truncate.pipe';
describe('TruncatePipe', () => {
const pipe = new TruncatePipe();
it('should truncate long text', () => {
const text = 'This is a fairly long text for the test';
expect(pipe.transform(text, 20)).toBe('This is a fairly lon...');
});
it('should not truncate short text', () => {
expect(pipe.transform('Hello', 20)).toBe('Hello');
});
it('should handle empty string', () => {
expect(pipe.transform('', 20)).toBe('');
});
});Testing components
For components, create a fixture that renders the component:
import { ComponentFixture, TestBed } from '@angular/core/testing';
describe('Counter', () => {
let fixture: ComponentFixture<Counter>;
let component: Counter;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [Counter],
}).compileComponents();
fixture = TestBed.createComponent(Counter);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should render the initial value', () => {
const element = fixture.nativeElement;
expect(element.textContent).toContain('0');
});
});Mocks and stubs
To isolate tests, use dependency mocks:
const mockAuth = {
user: signal(null),
isAuthenticated: computed(() => false),
login: vi.fn(),
};
TestBed.configureTestingModule({
providers: [
MyService,
{ provide: AuthService, useValue: mockAuth },
],
});Testing best practices
- One assert per test — Each test verifies a single thing
- Descriptive names —
should increment the value when increment() is called - Arrange-Act-Assert — Clear structure in each test
- Test behavior — Not internal implementation
- Start with services — They are the easiest and most valuable
Practice
- Test a service with signals: Create a
NotesServicewithadd(),remove()methods and acomputed()for the total. Write at least 3 unit tests that verify the signal behavior. - Test a pipe: Implement a
capitalizepipe and write tests for normal text, empty text, and text with multiple words. - Use mocks in a test: Create a test for a service that depends on
AuthService. Use a mock with{ provide: AuthService, useValue: mockAuth }to isolate the dependency.
In the next lesson, you will build a complete project that integrates everything you have learned.
// --- counter.service.ts ---
import { Injectable, signal, computed } from '@angular/core';
@Injectable({ providedIn: 'root' })
export class CounterService {
private readonly _value = signal(0);
readonly value = this._value.asReadonly();
readonly isEven = computed(() => this._value() % 2 === 0);
increment(): void {
this._value.update(v => v + 1);
}
decrement(): void {
this._value.update(v => Math.max(0, v - 1));
}
reset(): void {
this._value.set(0);
}
}
// --- counter.service.spec.ts ---
import { describe, it, expect, beforeEach } from 'vitest';
import { TestBed } from '@angular/core/testing';
import { CounterService } from './counter.service';
describe('CounterService', () => {
let service: CounterService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(CounterService);
});
it('should start at 0', () => {
expect(service.value()).toBe(0);
});
it('should increment the value', () => {
service.increment();
service.increment();
expect(service.value()).toBe(2);
});
it('should not decrement below 0', () => {
service.decrement();
expect(service.value()).toBe(0);
});
it('isEven should derive correctly', () => {
expect(service.isEven()).toBe(true);
service.increment();
expect(service.isEven()).toBe(false);
service.increment();
expect(service.isEven()).toBe(true);
});
it('should reset to 0', () => {
service.increment();
service.increment();
service.reset();
expect(service.value()).toBe(0);
});
});
Sign in to track your progress