Docs
/
Angular
Chapter 16
16 — Testing
Testing Pyramid in Angular
/ E2E Tests \ ← Few (Cypress/Playwright)
/ Integration \ ← Some (Component + Service tests)
/ Unit Tests \ ← Many (Pure logic, pipes, services)
Unit Testing Setup
Angular CLI ships with Jasmine + Karma by default. Many teams switch to Jest.
Switch to Jest
npm install --save-dev jest @angular-builders/jest @types/jest
npm uninstall karma karma-chrome-launcher karma-coverage karma-jasmine karma-jasmine-html-reporter
// angular.json
"test": {
"builder": "@angular-builders/jest:run",
"options": { "tsConfig": "tsconfig.spec.json" }
}
ng test # Run tests
ng test --watch # Watch mode
ng test --coverage # Coverage report
Testing Services
describe('UserService', () => {
let service: UserService;
let httpMock: HttpTestingController;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
UserService,
provideHttpClient(),
provideHttpClientTesting(),
],
});
service = TestBed.inject(UserService);
httpMock = TestBed.inject(HttpTestingController);
});
afterEach(() => {
httpMock.verify(); // Ensure no unmatched requests
});
it('should fetch users', () => {
const mockUsers: User[] = [
{ id: '1', name: 'Alice', email: 'alice@test.com' },
];
service.getAll().subscribe(users => {
expect(users).toEqual(mockUsers);
expect(users.length).toBe(1);
});
const req = httpMock.expectOne('/api/users');
expect(req.request.method).toBe('GET');
req.flush(mockUsers); // Respond with mock data
});
it('should handle errors', () => {
service.getAll().subscribe({
error: (err) => expect(err.status).toBe(500),
});
const req = httpMock.expectOne('/api/users');
req.flush('Server Error', { status: 500, statusText: 'Error' });
});
});
Testing Components
Basic Component Test
describe('UserCardComponent', () => {
let component: UserCardComponent;
let fixture: ComponentFixture<UserCardComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [UserCardComponent],
}).compileComponents();
fixture = TestBed.createComponent(UserCardComponent);
component = fixture.componentInstance;
});
it('should display user name', () => {
component.user = { id: '1', name: 'Alice', email: 'alice@test.com' };
fixture.detectChanges();
const el: HTMLElement = fixture.nativeElement;
expect(el.querySelector('h3')?.textContent).toContain('Alice');
});
it('should emit selected event on click', () => {
const user = { id: '1', name: 'Alice', email: 'alice@test.com' };
component.user = user;
fixture.detectChanges();
let emitted: User | undefined;
component.selected.subscribe(u => emitted = u);
const button = fixture.nativeElement.querySelector('button');
button.click();
expect(emitted).toEqual(user);
});
});
Component with Dependencies
describe('DashboardComponent', () => {
let component: DashboardComponent;
let fixture: ComponentFixture<DashboardComponent>;
let mockUserService: jasmine.SpyObj<UserService>;
beforeEach(async () => {
mockUserService = jasmine.createSpyObj('UserService', ['getAll']);
mockUserService.getAll.and.returnValue(of([{ id: '1', name: 'Alice' }]));
await TestBed.configureTestingModule({
imports: [DashboardComponent],
providers: [
{ provide: UserService, useValue: mockUserService },
],
}).compileComponents();
fixture = TestBed.createComponent(DashboardComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should load users on init', () => {
expect(mockUserService.getAll).toHaveBeenCalled();
expect(component.users.length).toBe(1);
});
});
Testing Reactive Forms
describe('LoginFormComponent', () => {
let component: LoginFormComponent;
let fixture: ComponentFixture<LoginFormComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [LoginFormComponent],
}).compileComponents();
fixture = TestBed.createComponent(LoginFormComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should be invalid when empty', () => {
expect(component.form.valid).toBeFalse();
});
it('should be valid with correct data', () => {
component.form.patchValue({
email: 'alice@test.com',
password: 'password123',
});
expect(component.form.valid).toBeTrue();
});
it('should show email error when invalid', () => {
const email = component.form.controls['email'];
email.setValue('invalid');
email.markAsTouched();
fixture.detectChanges();
const errorEl = fixture.nativeElement.querySelector('.error');
expect(errorEl).toBeTruthy();
});
});
Testing Signals
describe('CounterComponent', () => {
it('should increment count', () => {
const fixture = TestBed.createComponent(CounterComponent);
const component = fixture.componentInstance;
expect(component.count()).toBe(0);
component.increment();
expect(component.count()).toBe(1);
expect(component.double()).toBe(2);
});
});
Testing Pipes
describe('TruncatePipe', () => {
const pipe = new TruncatePipe();
it('should truncate long strings', () => {
expect(pipe.transform('Hello World', 5)).toBe('Hello...');
});
it('should not truncate short strings', () => {
expect(pipe.transform('Hi', 5)).toBe('Hi');
});
it('should handle empty string', () => {
expect(pipe.transform('', 5)).toBe('');
});
});
Testing Guards
describe('authGuard', () => {
it('should allow authenticated users', () => {
TestBed.configureTestingModule({
providers: [
{ provide: AuthService, useValue: { isAuthenticated: () => true } },
provideRouter([]),
],
});
const result = TestBed.runInInjectionContext(() =>
authGuard({} as any, { url: '/dashboard' } as any),
);
expect(result).toBeTrue();
});
it('should redirect unauthenticated users', () => {
TestBed.configureTestingModule({
providers: [
{ provide: AuthService, useValue: { isAuthenticated: () => false } },
provideRouter([]),
],
});
const result = TestBed.runInInjectionContext(() =>
authGuard({} as any, { url: '/dashboard' } as any),
);
expect(result).toBeInstanceOf(UrlTree);
});
});
E2E Testing with Playwright
ng add @angular/playwright
# or
npm install --save-dev @playwright/test
npx playwright install
// e2e/login.spec.ts
import { test, expect } from '@playwright/test';
test.describe('Login Page', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/login');
});
test('should display login form', async ({ page }) => {
await expect(page.locator('h1')).toHaveText('Login');
await expect(page.locator('input[name="email"]')).toBeVisible();
});
test('should login with valid credentials', async ({ page }) => {
await page.fill('input[name="email"]', 'alice@test.com');
await page.fill('input[name="password"]', 'password123');
await page.click('button[type="submit"]');
await expect(page).toHaveURL('/dashboard');
await expect(page.locator('.welcome')).toContainText('Alice');
});
test('should show error for invalid credentials', async ({ page }) => {
await page.fill('input[name="email"]', 'wrong@test.com');
await page.fill('input[name="password"]', 'wrong');
await page.click('button[type="submit"]');
await expect(page.locator('.error')).toBeVisible();
});
});
Key Takeaways
- Write many unit tests (services, pipes, pure logic), some component tests, few E2E tests
- Use
HttpTestingControllerto mock HTTP — never call real APIs in unit tests - Use
jasmine.createSpyObjor Jest mocks to mock service dependencies fixture.detectChanges()triggers change detection — call it after setting inputs- Test signals directly by calling
signal()— no async complexity TestBed.runInInjectionContext()is needed for testing functional guards/interceptors- Prefer Playwright over Protractor (deprecated) for E2E tests