Testing in Angular
Testing is the process of verifying that application code behaves as expected. Writing tests is not just a quality assurance practice — it is a design discipline that leads to better, more modular code. Angular is built with testability in mind, and comes with all the tools needed to write tests without additional configuration.
Angular supports two primary types of testing:
- Unit Tests — Test individual functions, classes, or components in isolation.
- End-to-End (E2E) Tests — Test the entire application by simulating real user interactions in a browser.
Tools Included with Angular
Jasmine
Jasmine is the testing framework used to write test cases. It provides functions for describing tests (describe), writing individual test cases (it), and making assertions (expect).
Karma
Karma is a test runner that executes the Jasmine tests in a real browser (Chrome by default) and reports the results in the terminal. Running ng test starts Karma.
TestBed
TestBed is Angular's testing utility. It creates a minimal Angular testing environment (a test module) where components, services, and other Angular features can be tested with full Angular behavior — including dependency injection, templates, and change detection.
Running Tests
ng test ← Run all tests and watch for changes
ng test --once ← Run all tests once and exit (for CI pipelines)
ng test --code-coverage ← Generate a code coverage report
Writing Unit Tests for a Service
Services are the easiest to test because they have no template — only TypeScript logic. Testing a service directly without TestBed is often the simplest approach:
// calculator.service.ts
import { Injectable } from '@angular/core';
@Injectable({ providedIn: 'root' })
export class CalculatorService {
add(a: number, b: number): number {
return a + b;
}
subtract(a: number, b: number): number {
return a - b;
}
multiply(a: number, b: number): number {
return a * b;
}
divide(a: number, b: number): number {
if (b === 0) {
throw new Error('Cannot divide by zero');
}
return a / b;
}
}
// calculator.service.spec.ts
import { CalculatorService } from './calculator.service';
describe('CalculatorService', () => {
let service: CalculatorService;
beforeEach(() => {
service = new CalculatorService(); // Create a fresh instance before each test
});
it('should be created', () => {
expect(service).toBeTruthy();
});
it('should add two numbers correctly', () => {
const result = service.add(5, 3);
expect(result).toBe(8);
});
it('should subtract two numbers correctly', () => {
const result = service.subtract(10, 4);
expect(result).toBe(6);
});
it('should multiply two numbers correctly', () => {
expect(service.multiply(3, 7)).toBe(21);
});
it('should divide two numbers correctly', () => {
expect(service.divide(20, 4)).toBe(5);
});
it('should throw an error when dividing by zero', () => {
expect(() => service.divide(10, 0)).toThrowError('Cannot divide by zero');
});
it('should return negative number for negative subtraction', () => {
expect(service.subtract(3, 10)).toBe(-7);
});
});
Test Structure Explained
describe()
Groups related tests under a label. Usually named after the class or feature being tested.
it()
Defines a single test case. The description should read as a sentence: "should add two numbers correctly."
beforeEach()
Runs setup code before each test. This ensures each test starts with a clean, fresh state.
expect()
Makes an assertion — verifies that a value meets a condition. Common matchers include toBe(), toEqual(), toBeTruthy(), toBeFalsy(), toContain(), toThrowError().
Testing a Service with HTTP Requests
When testing services that use HttpClient, Angular provides HttpClientTestingModule to intercept and control HTTP requests without making real network calls:
// post.service.spec.ts
import { TestBed } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { PostService } from './post.service';
describe('PostService', () => {
let service: PostService;
let httpMock: HttpTestingController;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule], // Use the test version of HttpClient
providers: [PostService]
});
service = TestBed.inject(PostService);
httpMock = TestBed.inject(HttpTestingController);
});
afterEach(() => {
httpMock.verify(); // Verify no unexpected HTTP requests were made
});
it('should fetch all posts', () => {
const mockPosts = [
{ id: 1, title: 'First Post', body: 'Content', userId: 1 },
{ id: 2, title: 'Second Post', body: 'Content', userId: 1 }
];
service.getAllPosts().subscribe(posts => {
expect(posts.length).toBe(2);
expect(posts[0].title).toBe('First Post');
});
// Intercept the HTTP request and return mock data
const req = httpMock.expectOne('https://jsonplaceholder.typicode.com/posts');
expect(req.request.method).toBe('GET');
req.flush(mockPosts); // Respond with mock data
});
});
Testing Components with TestBed
Component testing uses TestBed to create a minimal Angular environment that includes the component, its template, and its dependencies:
// counter.component.spec.ts
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { CounterComponent } from './counter.component';
describe('CounterComponent', () => {
let component: CounterComponent;
let fixture: ComponentFixture<CounterComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [CounterComponent]
}).compileComponents();
fixture = TestBed.createComponent(CounterComponent);
component = fixture.componentInstance;
fixture.detectChanges(); // Trigger initial change detection
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should start with count = 0', () => {
expect(component.count).toBe(0);
});
it('should increment count when increment() is called', () => {
component.increment();
expect(component.count).toBe(1);
component.increment();
expect(component.count).toBe(2);
});
it('should decrement count when decrement() is called', () => {
component.count = 5;
component.decrement();
expect(component.count).toBe(4);
});
it('should display the count in the template', () => {
component.count = 7;
fixture.detectChanges(); // Update the DOM after changing data
const compiled = fixture.nativeElement as HTMLElement;
expect(compiled.querySelector('h2')?.textContent).toContain('7');
});
it('should increment when the + button is clicked', () => {
const button = fixture.nativeElement.querySelector('button');
button.click();
fixture.detectChanges();
expect(component.count).toBe(1);
});
});
Key Testing Utilities
ComponentFixture
A wrapper around the component that provides access to the component instance, the native DOM element, and change detection control.
fixture.detectChanges()
Manually triggers Angular's change detection. Call this after changing data to update the DOM, or when interacting with the component and needing the template to reflect changes.
fixture.nativeElement
The underlying DOM element of the component. Use standard DOM methods like querySelector() to find elements and test their content.
Mocking Dependencies
When testing a component that uses a service, replace the real service with a mock to avoid testing both the component and the service simultaneously:
// products.component.spec.ts
import { TestBed, ComponentFixture } from '@angular/core/testing';
import { ProductsComponent } from './products.component';
import { ProductService } from '../services/product.service';
// Create a mock version of the service
const mockProductService = {
getAllProducts: jasmine.createSpy('getAllProducts').and.returnValue([
{ id: 1, name: 'Test Product', price: 29.99 }
])
};
describe('ProductsComponent', () => {
let component: ProductsComponent;
let fixture: ComponentFixture<ProductsComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ProductsComponent],
providers: [
{ provide: ProductService, useValue: mockProductService } // Inject mock
]
}).compileComponents();
fixture = TestBed.createComponent(ProductsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should load products from the service on init', () => {
expect(mockProductService.getAllProducts).toHaveBeenCalled();
expect(component.products.length).toBe(1);
expect(component.products[0].name).toBe('Test Product');
});
});
Common Jasmine Matchers Reference
expect(value).toBe(expected) ← Strict equality (===)
expect(value).toEqual(expected) ← Deep equality (objects/arrays)
expect(value).toBeTruthy() ← Any truthy value
expect(value).toBeFalsy() ← Any falsy value (null, 0, '', false)
expect(value).toBeNull() ← Strictly null
expect(value).toBeUndefined() ← Strictly undefined
expect(value).toContain(item) ← String/array contains item
expect(value).toBeGreaterThan(n) ← Number comparison
expect(value).toBeLessThan(n) ← Number comparison
expect(fn).toThrowError(msg) ← Function throws an error
expect(spy).toHaveBeenCalled() ← Spy was called
expect(spy).toHaveBeenCalledWith(args) ← Spy was called with specific arguments
expect(spy).toHaveBeenCalledTimes(n) ← Spy was called n times
Summary
Angular testing uses Jasmine for writing tests and Karma for running them in a browser. ng test starts the test runner. Unit tests for services are written using describe, it, beforeEach, and expect. Services that make HTTP requests are tested with HttpClientTestingModule and HttpTestingController to intercept and mock network requests. Component tests use TestBed to create a testing module, ComponentFixture to access the component and its DOM, and fixture.detectChanges() to trigger rendering. Dependencies like services are replaced with mock objects using providers in the test module configuration. Testing in isolation — one unit at a time — leads to reliable, meaningful test results.
