Skip to content

Lab 22: Unit Testing with Vitest

album-wholesale-v17-signal-pre-jest

In this lab you will write unit tests for services and components using Vitest, Angular’s recommended modern test runner. You will use vi.fn() for mocking, Vitest matchers, and lifecycle hooks.

Install compatible versions manually (ng add @angular/vitest installs Vitest 4.x which conflicts with the Angular 17 / Vite 5 toolchain):

Terminal window
npm install --save-dev vitest@2 @vitest/coverage-v8@2 @analogjs/vite-plugin-angular jsdom

Create vite.config.ts at the project root:

import { defineConfig } from 'vitest/config';
import angular from '@analogjs/vite-plugin-angular';
export default defineConfig({
plugins: [angular()],
test: {
globals: true,
environment: 'jsdom',
include: ['**/*.spec.ts'],
coverage: {
provider: 'v8',
reporter: ['text', 'html'],
},
},
});

Add a test script to package.json:

{
"scripts": {
"test": "vitest run",
"test:watch": "vitest",
"test:coverage": "vitest run --coverage"
}
}
Terminal window
npm test

Check that all existing tests pass before adding new ones.

Create src/app/services/album.service.spec.ts:

import { TestBed } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController }
from '@angular/common/http/testing';
import { AlbumService } from './album.service';
import { Album } from '../model/album.model';
describe('AlbumService', () => {
let service: AlbumService;
let httpMock: HttpTestingController;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [AlbumService],
});
service = TestBed.inject(AlbumService);
httpMock = TestBed.inject(HttpTestingController);
});
afterEach(() => httpMock.verify());
it('should fetch albums', () => {
const mockAlbums: Album[] = [
{ id: 1, name: 'Test Album', artist: 'Artist', price: 9, description: '', tags: [] },
];
service.findAll().subscribe(albums => {
expect(albums).toHaveLength(1);
expect(albums[0].name).toBe('Test Album');
});
const req = httpMock.expectOne('http://localhost:3000/albums');
expect(req.request.method).toBe('GET');
req.flush(mockAlbums);
});
});

Create src/app/components/album-list/album-list.component.spec.ts:

import { ComponentFixture, TestBed } from '@angular/core/testing';
import { vi } from 'vitest';
import { of } from 'rxjs';
import { AlbumListComponent } from './album-list.component';
import { AlbumService } from '../../services/album.service';
describe('AlbumListComponent', () => {
let fixture: ComponentFixture<AlbumListComponent>;
let albumServiceMock: { findAll: ReturnType<typeof vi.fn> };
beforeEach(async () => {
albumServiceMock = {
findAll: vi.fn().mockReturnValue(of([
{ id: 1, name: 'Mock Album', artist: 'Mock Artist', price: 9, description: '', tags: [] },
])),
};
await TestBed.configureTestingModule({
imports: [AlbumListComponent],
providers: [
{ provide: AlbumService, useValue: albumServiceMock },
],
}).compileComponents();
fixture = TestBed.createComponent(AlbumListComponent);
fixture.detectChanges();
});
it('should display albums', () => {
const items = fixture.nativeElement.querySelectorAll('.album-card');
expect(items.length).toBeGreaterThan(0);
});
it('should call findAll on init', () => {
expect(albumServiceMock.findAll).toHaveBeenCalledOnce();
});
});

Vitest provides beforeAll, beforeEach, afterEach, afterAll:

describe('Cart calculations', () => {
let items: CartItem[];
beforeEach(() => {
items = [
{ id: 1, quantity: 2, price: 10 },
{ id: 2, quantity: 1, price: 20 },
];
});
it('should calculate total', () => {
const total = items.reduce((sum, i) => sum + i.quantity * i.price, 0);
expect(total).toBe(40);
});
it('should count items', () => {
expect(items).toHaveLength(2);
});
});
Terminal window
npx vitest --coverage

Check the coverage report and ensure critical service methods are covered.

Terminal window
npx vitest --ui

Explore the interactive test browser to navigate tests and see results in real time.