Unit Testing
Running Tests (Vitest)
# Run tests once
npm run test
# Watch mode
npm run test:watch
# Run with coverage
npm run test:coverage
# Run specific test file
npm run test album.component.spec.ts
# UI mode
npm run test:ui
Setup Vitest
Install Vitest and Angular testing utilities:
npm install -D vitest @vitest/ui @angular/platform-browser-dynamic
npm install -D @analogjs/vite-plugin-angular
Configure vite.config.ts:
import { defineConfig } from 'vitest/config';
import angular from '@analogjs/vite-plugin-angular';
export default defineConfig({
plugins: [angular()],
test: {
globals: true,
environment: 'jsdom',
setupFiles: ['src/test-setup.ts'],
include: ['**/*.spec.ts'],
coverage: {
provider: 'v8',
reporter: ['text', 'html', 'lcov']
}
}
});
Basic Component Test
Component:
import { Component, input, output } from '@angular/core';
@Component({
selector: 'app-album',
standalone: true,
template: `
<h1>{{ title() }}</h1>
<button (click)="play()">Play</button>
`
})
export class AlbumComponent {
title = input<string>('');
played = output<void>();
play() {
this.played.emit();
}
}
Test:
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { AlbumComponent } from './album.component';
describe('AlbumComponent', () => {
let component: AlbumComponent;
let fixture: ComponentFixture<AlbumComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [AlbumComponent]
}).compileComponents();
fixture = TestBed.createComponent(AlbumComponent);
component = fixture.componentInstance;
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should display title', () => {
fixture.componentRef.setInput('title', 'Dark Side');
fixture.detectChanges();
const h1 = fixture.nativeElement.querySelector('h1');
expect(h1.textContent).toBe('Dark Side');
});
it('should emit on button click', () => {
const playSpy = vi.fn();
component.played.subscribe(playSpy);
const button = fixture.nativeElement.querySelector('button');
button.click();
expect(playSpy).toHaveBeenCalled();
});
});
Testing Services
Service:
@Injectable({ providedIn: 'root' })
export class AlbumService {
private http = inject(HttpClient);
getAlbums(): Observable<Album[]> {
return this.http.get<Album[]>('/api/albums');
}
}
Test:
import { TestBed } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { AlbumService } from './album.service';
describe('AlbumService', () => {
let service: AlbumService;
let httpMock: HttpTestingController;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule]
});
service = TestBed.inject(AlbumService);
httpMock = TestBed.inject(HttpTestingController);
});
afterEach(() => {
httpMock.verify();
});
it('should get albums', () => {
const mockAlbums = [
{ id: 1, name: 'Abbey Road', artist: 'The Beatles' }
];
service.getAlbums().subscribe(albums => {
expect(albums).toEqual(mockAlbums);
});
const req = httpMock.expectOne('/api/albums');
expect(req.request.method).toBe('GET');
req.flush(mockAlbums);
});
});
Testing with Mocks
import { describe, it, expect, vi } from 'vitest';
describe('AlbumListComponent', () => {
it('should load albums', () => {
const mockService = {
getAlbums: vi.fn().mockReturnValue(of([{ id: 1, name: 'Album' }]))
};
TestBed.configureTestingModule({
imports: [AlbumListComponent],
providers: [
{ provide: AlbumService, useValue: mockService }
]
});
const fixture = TestBed.createComponent(AlbumListComponent);
fixture.detectChanges();
expect(mockService.getAlbums).toHaveBeenCalled();
expect(fixture.componentInstance.albums).toHaveLength(1);
});
});
Testing Async Code
import { describe, it, expect, vi } from 'vitest';
import { fakeAsync, tick } from '@angular/core/testing';
it('should debounce search', fakeAsync(() => {
component.search('test');
tick(299);
expect(searchSpy).not.toHaveBeenCalled();
tick(1); // 300ms total
expect(searchSpy).toHaveBeenCalledWith('test');
}));
Vitest Matchers
Common assertions:
// Basic
expect(value).toBe(expected);
expect(value).toEqual(expected);
expect(value).toBeTruthy();
expect(value).toBeFalsy();
expect(value).toBeNull();
expect(value).toBeUndefined();
// Numbers
expect(value).toBeGreaterThan(3);
expect(value).toBeLessThan(5);
// Strings
expect(text).toContain('substring');
expect(text).toMatch(/pattern/);
// Arrays
expect(array).toHaveLength(3);
expect(array).toContain(item);
// Functions
expect(fn).toHaveBeenCalled();
expect(fn).toHaveBeenCalledWith(arg1, arg2);
expect(fn).toHaveBeenCalledTimes(2);
Best Practices
- ✅ Use
describeto group related tests - ✅ Use
beforeEachfor common setup - ✅ Test behavior, not implementation
- ✅ Mock external dependencies
- ✅ Use descriptive test names
- ✅ Aim for 80%+ coverage
- ✅ Test edge cases (null, empty, errors)