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.
Test logic, not templates
Focus on testing services and business logic first. Component tests with templates are more fragile and expensive to maintain. A well-tested service is more valuable than many UI tests.
Arrange-Act-Assert
Structure your tests in three phases: Arrange (prepare data), Act (execute the action), and Assert (verify results). This makes tests clear and easy to read.
// --- 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