Overview
Angular provides a robust testing infrastructure built on Jasmine and Karma (or Jest). The testing utilities make it easy to write unit tests, integration tests, and end-to-end tests for your applications.Testing Setup
Basic Test Structure
Angular tests follow a standard pattern using Jasmine’sdescribe and it blocks.
import { TestBed } from '@angular/core/testing';
import { MyComponent } from './my.component';
describe('MyComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [MyComponent]
}).compileComponents();
});
it('should create', () => {
const fixture = TestBed.createComponent(MyComponent);
const component = fixture.componentInstance;
expect(component).toBeTruthy();
});
});
TestBed
TestBed is Angular’s primary testing utility that creates a testing module for your components.Configuring TestBed
import { TestBed } from '@angular/core/testing';
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { UserService } from './user.service';
import { UserComponent } from './user.component';
describe('UserComponent', () => {
let service: UserService;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [
UserComponent,
HttpClientTestingModule,
BrowserAnimationsModule
],
providers: [
UserService
]
}).compileComponents();
service = TestBed.inject(UserService);
});
it('should inject service', () => {
expect(service).toBeTruthy();
});
});
Standalone Components
import { TestBed } from '@angular/core/testing';
import { provideHttpClient } from '@angular/common/http';
import { provideAnimations } from '@angular/platform-browser/animations';
import { StandaloneComponent } from './standalone.component';
describe('StandaloneComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [StandaloneComponent],
providers: [
provideHttpClient(),
provideAnimations()
]
}).compileComponents();
});
it('should work', () => {
const fixture = TestBed.createComponent(StandaloneComponent);
expect(fixture.componentInstance).toBeTruthy();
});
});
ComponentFixture
ComponentFixture provides access to the component instance and its DOM.Basic Usage
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { CounterComponent } from './counter.component';
describe('CounterComponent', () => {
let component: CounterComponent;
let fixture: ComponentFixture<CounterComponent>;
let compiled: HTMLElement;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [CounterComponent]
}).compileComponents();
fixture = TestBed.createComponent(CounterComponent);
component = fixture.componentInstance;
compiled = fixture.nativeElement;
});
it('should display counter value', async () => {
component.count.set(5);
await fixture.whenStable();
expect(compiled.textContent).toContain('Count: 5');
});
it('should increment counter', async () => {
component.increment();
await fixture.whenStable();
expect(component.count()).toBe(1);
});
});
Act, Wait, Assert Pattern
For zoneless and modern Angular applications, use the Act-Wait-Assert pattern.import { ComponentFixture, TestBed } from '@angular/core/testing';
import { provideZonelessChangeDetection } from '@angular/core';
import { AsyncComponent } from './async.component';
describe('AsyncComponent (Zoneless)', () => {
let fixture: ComponentFixture<AsyncComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [AsyncComponent],
providers: [
provideZonelessChangeDetection()
]
}).compileComponents();
fixture = TestBed.createComponent(AsyncComponent);
});
it('should load data', async () => {
// Act: Trigger the action
fixture.componentInstance.loadData();
// Wait: Wait for async operations to complete
await fixture.whenStable();
// Assert: Verify the result
expect(fixture.componentInstance.data()).toBeTruthy();
expect(fixture.nativeElement.textContent).toContain('Loaded');
});
});
In zoneless applications, always use
await fixture.whenStable() instead of fixture.detectChanges().Testing Services
Simple Service
import { TestBed } from '@angular/core/testing';
import { CalculatorService } from './calculator.service';
describe('CalculatorService', () => {
let service: CalculatorService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(CalculatorService);
});
it('should add numbers', () => {
expect(service.add(2, 3)).toBe(5);
});
it('should subtract numbers', () => {
expect(service.subtract(5, 3)).toBe(2);
});
it('should multiply numbers', () => {
expect(service.multiply(4, 3)).toBe(12);
});
});
Service with Dependencies
import { TestBed } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { UserService } from './user.service';
interface User {
id: number;
name: string;
}
describe('UserService', () => {
let service: UserService;
let httpMock: HttpTestingController;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [UserService]
});
service = TestBed.inject(UserService);
httpMock = TestBed.inject(HttpTestingController);
});
afterEach(() => {
httpMock.verify();
});
it('should fetch users', () => {
const mockUsers: User[] = [
{ id: 1, name: 'John' },
{ id: 2, name: 'Jane' }
];
service.getUsers().subscribe(users => {
expect(users.length).toBe(2);
expect(users).toEqual(mockUsers);
});
const req = httpMock.expectOne('/api/users');
expect(req.request.method).toBe('GET');
req.flush(mockUsers);
});
it('should handle errors', () => {
service.getUsers().subscribe({
next: () => fail('should have failed'),
error: (error) => {
expect(error.status).toBe(404);
}
});
const req = httpMock.expectOne('/api/users');
req.flush('Not found', { status: 404, statusText: 'Not Found' });
});
});
Testing Components with Signals
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { signal } from '@angular/core';
import { TodoComponent } from './todo.component';
describe('TodoComponent with Signals', () => {
let component: TodoComponent;
let fixture: ComponentFixture<TodoComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [TodoComponent]
}).compileComponents();
fixture = TestBed.createComponent(TodoComponent);
component = fixture.componentInstance;
});
it('should add todo', async () => {
const initialCount = component.todos().length;
component.addTodo('New task');
await fixture.whenStable();
expect(component.todos().length).toBe(initialCount + 1);
expect(component.todos()[initialCount].title).toBe('New task');
});
it('should toggle todo', async () => {
component.addTodo('Task 1');
await fixture.whenStable();
const todoId = component.todos()[0].id;
component.toggleTodo(todoId);
await fixture.whenStable();
expect(component.todos()[0].completed).toBe(true);
});
it('should compute stats correctly', async () => {
component.addTodo('Task 1');
component.addTodo('Task 2');
await fixture.whenStable();
expect(component.stats().total).toBe(2);
expect(component.stats().active).toBe(2);
expect(component.stats().completed).toBe(0);
component.toggleTodo(component.todos()[0].id);
await fixture.whenStable();
expect(component.stats().active).toBe(1);
expect(component.stats().completed).toBe(1);
});
});
Mocking Dependencies
Mock Services
import { TestBed } from '@angular/core/testing';
import { of } from 'rxjs';
import { DataService } from './data.service';
import { DataComponent } from './data.component';
describe('DataComponent with Mock', () => {
let mockDataService: jasmine.SpyObj<DataService>;
beforeEach(async () => {
mockDataService = jasmine.createSpyObj('DataService', ['getData']);
mockDataService.getData.and.returnValue(of(['item1', 'item2']));
await TestBed.configureTestingModule({
imports: [DataComponent],
providers: [
{ provide: DataService, useValue: mockDataService }
]
}).compileComponents();
});
it('should load data from service', async () => {
const fixture = TestBed.createComponent(DataComponent);
await fixture.whenStable();
expect(mockDataService.getData).toHaveBeenCalled();
expect(fixture.componentInstance.items.length).toBe(2);
});
});
Mock Providers
import { TestBed } from '@angular/core/testing';
import { AuthService } from './auth.service';
import { ProtectedComponent } from './protected.component';
class MockAuthService {
isAuthenticated = true;
currentUser = { id: 1, name: 'Test User' };
login(credentials: any) {
return Promise.resolve(this.currentUser);
}
logout() {
this.isAuthenticated = false;
}
}
describe('ProtectedComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ProtectedComponent],
providers: [
{ provide: AuthService, useClass: MockAuthService }
]
}).compileComponents();
});
it('should show content when authenticated', async () => {
const fixture = TestBed.createComponent(ProtectedComponent);
await fixture.whenStable();
expect(fixture.nativeElement.textContent).toContain('Protected Content');
});
});
Testing User Interactions
Click Events
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ButtonComponent } from './button.component';
describe('ButtonComponent', () => {
let fixture: ComponentFixture<ButtonComponent>;
let button: HTMLButtonElement;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ButtonComponent]
}).compileComponents();
fixture = TestBed.createComponent(ButtonComponent);
button = fixture.nativeElement.querySelector('button');
});
it('should handle click', async () => {
const component = fixture.componentInstance;
spyOn(component, 'onClick');
button.click();
await fixture.whenStable();
expect(component.onClick).toHaveBeenCalled();
});
it('should be disabled when loading', async () => {
fixture.componentInstance.loading.set(true);
await fixture.whenStable();
expect(button.disabled).toBe(true);
});
});
Form Input
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { FormsModule } from '@angular/forms';
import { InputComponent } from './input.component';
describe('InputComponent', () => {
let fixture: ComponentFixture<InputComponent>;
let input: HTMLInputElement;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [InputComponent, FormsModule]
}).compileComponents();
fixture = TestBed.createComponent(InputComponent);
input = fixture.nativeElement.querySelector('input');
});
it('should update on input', async () => {
input.value = 'test value';
input.dispatchEvent(new Event('input'));
await fixture.whenStable();
expect(fixture.componentInstance.value()).toBe('test value');
});
});
Async Testing
Testing Observables
import { TestBed, fakeAsync, tick } from '@angular/core/testing';
import { of, delay } from 'rxjs';
import { AsyncService } from './async.service';
describe('AsyncService', () => {
let service: AsyncService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(AsyncService);
});
it('should return data after delay', fakeAsync(() => {
let result: string | undefined;
service.getDataWithDelay().subscribe(data => {
result = data;
});
expect(result).toBeUndefined();
tick(1000); // Fast-forward time
expect(result).toBe('delayed data');
}));
});
Testing Promises
import { TestBed } from '@angular/core/testing';
import { ApiService } from './api.service';
describe('ApiService', () => {
let service: ApiService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(ApiService);
});
it('should fetch data', async () => {
const data = await service.fetchData();
expect(data).toBeTruthy();
});
it('should handle errors', async () => {
try {
await service.fetchInvalidData();
fail('should have thrown error');
} catch (error) {
expect(error).toBeTruthy();
}
});
});
Testing Pipes
import { TitleCasePipe } from './title-case.pipe';
describe('TitleCasePipe', () => {
let pipe: TitleCasePipe;
beforeEach(() => {
pipe = new TitleCasePipe();
});
it('should transform text to title case', () => {
expect(pipe.transform('hello world')).toBe('Hello World');
});
it('should handle empty string', () => {
expect(pipe.transform('')).toBe('');
});
it('should handle single word', () => {
expect(pipe.transform('hello')).toBe('Hello');
});
});
Testing Directives
import { Component } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { HighlightDirective } from './highlight.directive';
@Component({
template: `<p appHighlight>Test</p>`,
standalone: true,
imports: [HighlightDirective]
})
class TestComponent {}
describe('HighlightDirective', () => {
let fixture: ComponentFixture<TestComponent>;
let element: HTMLElement;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [TestComponent]
}).compileComponents();
fixture = TestBed.createComponent(TestComponent);
element = fixture.nativeElement.querySelector('p');
});
it('should highlight element', async () => {
await fixture.whenStable();
expect(element.style.backgroundColor).toBe('yellow');
});
});
Best Practices
Use Act-Wait-Assert
Follow the Act-Wait-Assert pattern for reliable async testing.
Mock Dependencies
Mock external dependencies to isolate units under test.
Test Behavior, Not Implementation
Focus on what components do, not how they do it.
Keep Tests Fast
Use
fakeAsync and tick() to avoid real time delays.Avoid testing private methods directly. Test public API and observable behavior instead.
Code Coverage
Run tests with coverage reporting:ng test --code-coverage
karma.conf.js:
coverageReporter: {
type: 'html',
dir: require('path').join(__dirname, './coverage'),
check: {
global: {
statements: 80,
branches: 80,
functions: 80,
lines: 80
}
}
}
