Why Test?
Testing ensures your Angular application works correctly, catches bugs early, and gives you confidence when refactoring. Angular ships with a complete testing setup out of the box.
Testing Stack
| Tool | Purpose |
|---|---|
| Jasmine | Test framework (describe, it, expect) |
| Karma | Test runner (executes tests in a browser) |
| TestBed | Angular testing utility for configuring test modules |
| Angular CLI | ng test command to run tests |
Running Tests
bash
# Run tests (opens browser, watches for changes)
ng test
# Run once without watching
ng test --watch=false
# Run with code coverage report
ng test --code-coverage
# Run specific test file
ng test --include=src/app/services/user.service.spec.tsTesting Services
Simple Service Test
typescript
// calculator.service.ts
@Injectable({ providedIn: 'root' })
export class CalculatorService {
add(a: number, b: number): number { return a + b; }
divide(a: number, b: number): number {
if (b === 0) throw new Error('Division by zero');
return a / b;
}
}typescript
// calculator.service.spec.ts
describe('CalculatorService', () => {
let service: CalculatorService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(CalculatorService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
it('should add two numbers', () => {
expect(service.add(2, 3)).toBe(5);
expect(service.add(-1, 1)).toBe(0);
});
it('should divide two numbers', () => {
expect(service.divide(10, 2)).toBe(5);
});
it('should throw on division by zero', () => {
expect(() => service.divide(10, 0)).toThrowError('Division by zero');
});
});Service with HTTP Dependency
typescript
// user.service.ts
@Injectable({ providedIn: 'root' })
export class UserService {
private http = inject(HttpClient);
getUsers(): Observable<User[]> {
return this.http.get<User[]>('/api/users');
}
getUser(id: number): Observable<User> {
return this.http.get<User>(`/api/users/${id}`);
}
}typescript
// user.service.spec.ts
describe('UserService', () => {
let service: UserService;
let httpMock: HttpTestingController;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
});
service = TestBed.inject(UserService);
httpMock = TestBed.inject(HttpTestingController);
});
afterEach(() => {
httpMock.verify(); // Ensure no outstanding requests
});
it('should fetch all users', () => {
const mockUsers: User[] = [
{ id: 1, name: 'Alice', email: 'alice@test.com' },
{ id: 2, name: 'Bob', email: 'bob@test.com' },
];
service.getUsers().subscribe(users => {
expect(users.length).toBe(2);
expect(users[0].name).toBe('Alice');
});
const req = httpMock.expectOne('/api/users');
expect(req.request.method).toBe('GET');
req.flush(mockUsers); // Respond with mock data
});
it('should handle HTTP error', () => {
service.getUsers().subscribe({
next: () => fail('Should have failed'),
error: (error) => {
expect(error.status).toBe(500);
},
});
const req = httpMock.expectOne('/api/users');
req.flush('Server error', { status: 500, statusText: 'Internal Server Error' });
});
});Testing Components
Basic Component Test
typescript
// greeting.component.ts
@Component({
standalone: true,
template: `<h1>Hello, {{ name }}!</h1>`,
})
export class GreetingComponent {
@Input() name = 'World';
}typescript
// greeting.component.spec.ts
describe('GreetingComponent', () => {
let component: GreetingComponent;
let fixture: ComponentFixture<GreetingComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [GreetingComponent],
}).compileComponents();
fixture = TestBed.createComponent(GreetingComponent);
component = fixture.componentInstance;
fixture.detectChanges(); // Trigger initial change detection
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should display default greeting', () => {
const h1 = fixture.nativeElement.querySelector('h1');
expect(h1.textContent).toBe('Hello, World!');
});
it('should display custom name', () => {
component.name = 'Angular';
fixture.detectChanges();
const h1 = fixture.nativeElement.querySelector('h1');
expect(h1.textContent).toBe('Hello, Angular!');
});
});Component with User Interaction
typescript
// counter.component.spec.ts
describe('CounterComponent', () => {
let fixture: ComponentFixture<CounterComponent>;
let component: CounterComponent;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [CounterComponent],
}).compileComponents();
fixture = TestBed.createComponent(CounterComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should start at 0', () => {
const display = fixture.nativeElement.querySelector('.count');
expect(display.textContent).toContain('0');
});
it('should increment on button click', () => {
const button = fixture.nativeElement.querySelector('.increment');
button.click();
fixture.detectChanges();
const display = fixture.nativeElement.querySelector('.count');
expect(display.textContent).toContain('1');
});
it('should emit countChange event', () => {
spyOn(component.countChange, 'emit');
component.increment();
expect(component.countChange.emit).toHaveBeenCalledWith(1);
});
});Component with Injected Service
typescript
// user-list.component.spec.ts
describe('UserListComponent', () => {
let fixture: ComponentFixture<UserListComponent>;
let userServiceSpy: jasmine.SpyObj<UserService>;
beforeEach(async () => {
// Create a spy/mock for the service
const spy = jasmine.createSpyObj('UserService', ['getUsers']);
await TestBed.configureTestingModule({
imports: [UserListComponent],
providers: [
{ provide: UserService, useValue: spy },
],
}).compileComponents();
userServiceSpy = TestBed.inject(UserService) as jasmine.SpyObj<UserService>;
});
it('should display users when loaded', () => {
const mockUsers = [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' },
];
userServiceSpy.getUsers.and.returnValue(of(mockUsers));
fixture = TestBed.createComponent(UserListComponent);
fixture.detectChanges();
const items = fixture.nativeElement.querySelectorAll('.user-item');
expect(items.length).toBe(2);
expect(items[0].textContent).toContain('Alice');
});
it('should show loading state', () => {
userServiceSpy.getUsers.and.returnValue(new Observable()); // Never emits
fixture = TestBed.createComponent(UserListComponent);
fixture.detectChanges();
const loading = fixture.nativeElement.querySelector('.loading');
expect(loading).toBeTruthy();
});
});Testing Pipes
typescript
// truncate.pipe.ts
@Pipe({ name: 'truncate', standalone: true })
export class TruncatePipe implements PipeTransform {
transform(value: string, limit: number = 50): string {
return value.length > limit ? value.substring(0, limit) + '...' : value;
}
}typescript
// truncate.pipe.spec.ts
describe('TruncatePipe', () => {
const pipe = new TruncatePipe();
it('should not truncate short strings', () => {
expect(pipe.transform('Hello')).toBe('Hello');
});
it('should truncate long strings with default limit', () => {
const long = 'A'.repeat(60);
const result = pipe.transform(long);
expect(result.length).toBe(53); // 50 + '...'
expect(result.endsWith('...')).toBeTrue();
});
it('should truncate with custom limit', () => {
expect(pipe.transform('Hello World', 5)).toBe('Hello...');
});
});Testing with Async Operations
fakeAsync / tick
typescript
it('should debounce search input', fakeAsync(() => {
component.searchControl.setValue('ang');
tick(200); // Advance 200ms (debounce is 300ms)
expect(searchServiceSpy.search).not.toHaveBeenCalled();
tick(100); // Advance to 300ms total
expect(searchServiceSpy.search).toHaveBeenCalledWith('ang');
}));waitForAsync
typescript
it('should load data on init', waitForAsync(() => {
userServiceSpy.getUsers.and.returnValue(of(mockUsers));
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
const items = fixture.nativeElement.querySelectorAll('.user-item');
expect(items.length).toBe(2);
});
}));Common Matchers
typescript
// Equality
expect(value).toBe(5); // Strict ===
expect(value).toEqual({ a: 1 }); // Deep equality
// Truthiness
expect(value).toBeTruthy();
expect(value).toBeFalsy();
expect(value).toBeNull();
expect(value).toBeDefined();
expect(value).toBeUndefined();
// Comparison
expect(value).toBeGreaterThan(3);
expect(value).toBeLessThanOrEqual(10);
// Strings
expect(value).toContain('hello');
expect(value).toMatch(/pattern/);
// Arrays
expect(arr).toContain('item');
expect(arr).toHaveSize(3);
// Negation
expect(value).not.toBe(0);
// Spies
expect(spy).toHaveBeenCalled();
expect(spy).toHaveBeenCalledWith('arg1', 'arg2');
expect(spy).toHaveBeenCalledTimes(3);Best Practices
- Test behavior, not implementation — focus on what components do, not internal details
- Use
beforeEachto set up fresh instances for each test - Mock dependencies — services, HTTP calls, and external dependencies
- Keep tests independent — tests should not depend on each other
- Use meaningful test descriptions —
it('should display error when form is invalid') - Test edge cases — empty arrays, null values, error states
- Aim for meaningful coverage — focus on business logic and critical paths
Next Steps
With testing in place, let's explore performance optimization — making your Angular application faster with change detection strategies, lazy loading, and bundle optimization techniques.