Angular Jest Integration
Introduction
Testing is a crucial part of developing robust Angular applications. While Angular comes with Jasmine and Karma as its default testing tools, many developers prefer Jest for its simplicity, performance, and feature set. In this guide, we'll explore how to integrate Jest with Angular projects and leverage its powerful features for effective testing.
Jest is a delightful JavaScript testing framework maintained by Facebook that focuses on simplicity and support for large web applications. It offers features like snapshot testing, parallel test execution, and comprehensive mocking capabilities that make it an excellent choice for Angular applications.
Why Choose Jest over Karma/Jasmine?
Before diving into the integration process, let's understand why you might want to use Jest:
- Speed: Jest runs tests in parallel, making it significantly faster than Karma
- Snapshot Testing: Easily test UI components and verify they don't change unexpectedly
- Integrated Mocking: Powerful built-in mocking capabilities
- Interactive Watch Mode: Provides immediate feedback as you develop
- Zero Configuration: Works out of the box for most JavaScript projects
Setting Up Jest in an Angular Project
Let's walk through the process of integrating Jest into an existing Angular project.
Step 1: Install Required Dependencies
First, we need to install Jest and related packages:
npm install --save-dev jest @types/jest jest-preset-angular
Step 2: Configure Jest
Create a jest.config.js
file in your project root:
module.exports = {
preset: 'jest-preset-angular',
setupFilesAfterEnv: ['<rootDir>/setup-jest.ts'],
testPathIgnorePatterns: [
'<rootDir>/node_modules/',
'<rootDir>/dist/'
],
globals: {
'ts-jest': {
tsconfig: '<rootDir>/tsconfig.spec.json',
stringifyContentPathRegex: '\\.html$',
},
},
};
Step 3: Create Setup File
Create a setup-jest.ts
file in your project root:
import 'jest-preset-angular/setup-jest';
/* global mocks for jsdom */
const mock = () => {
let storage: {[key: string]: string} = {};
return {
getItem: (key: string) => (key in storage ? storage[key] : null),
setItem: (key: string, value: string) => (storage[key] = value || ''),
removeItem: (key: string) => delete storage[key],
clear: () => (storage = {})
};
};
Object.defineProperty(window, 'localStorage', { value: mock() });
Object.defineProperty(window, 'sessionStorage', { value: mock() });
Object.defineProperty(window, 'CSS', { value: null });
Step 4: Update tsconfig.spec.json
Update your tsconfig.spec.json
to include Jest types:
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/spec",
"types": [
"jest",
"node"
]
},
"files": [
"src/polyfills.ts"
],
"include": [
"src/**/*.spec.ts",
"src/**/*.d.ts"
]
}
Step 5: Configure Package.json Scripts
Update your package.json
scripts to use Jest instead of Karma:
{
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage"
}
}
Writing Tests with Jest in Angular
Now that we've set up Jest, let's look at how to write tests for Angular components, services, and pipes.
Testing a Simple Service
Let's create a simple data service and test it:
// data.service.ts
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class DataService {
getUsers() {
return Promise.resolve([
{ id: 1, name: 'John' },
{ id: 2, name: 'Jane' }
]);
}
}
Here's how we would test this service:
// data.service.spec.ts
import { TestBed } from '@angular/core/testing';
import { DataService } from './data.service';
describe('DataService', () => {
let service: DataService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(DataService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
it('should return users', async () => {
const users = await service.getUsers();
expect(users.length).toBe(2);
expect(users[0].name).toBe('John');
expect(users[1].name).toBe('Jane');
});
});
Testing a Component
Let's test a simple counter component:
// counter.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-counter',
template: `
<div>
<h1>Counter: {{ count }}</h1>
<button (click)="increment()">Increment</button>
<button (click)="decrement()">Decrement</button>
</div>
`
})
export class CounterComponent {
count = 0;
increment() {
this.count++;
}
decrement() {
this.count--;
}
}
And here's the test for this component:
// 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();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should have initial count of 0', () => {
expect(component.count).toBe(0);
});
it('should increment count when increment is called', () => {
component.increment();
expect(component.count).toBe(1);
});
it('should decrement count when decrement is called', () => {
component.decrement();
expect(component.count).toBe(-1);
});
it('should update the displayed count after increment', () => {
component.increment();
fixture.detectChanges();
const compiled = fixture.nativeElement;
expect(compiled.querySelector('h1').textContent).toContain('Counter: 1');
});
});
Using Jest Snapshot Testing
One of Jest's most powerful features is snapshot testing, which is especially useful for UI components.
// welcome.component.ts
import { Component, Input } from '@angular/core';
@Component({
selector: 'app-welcome',
template: `
<div class="welcome">
<h1>Welcome {{ name }}!</h1>
<p>We're glad to have you here.</p>
<div *ngIf="showDetails">
<p>Your membership ID is {{ id }}</p>
<p>Status: {{ status }}</p>
</div>
</div>
`
})
export class WelcomeComponent {
@Input() name = 'Guest';
@Input() id = '';
@Input() status = '';
@Input() showDetails = false;
}
Let's test using snapshots:
// welcome.component.spec.ts
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { WelcomeComponent } from './welcome.component';
describe('WelcomeComponent', () => {
let component: WelcomeComponent;
let fixture: ComponentFixture<WelcomeComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [WelcomeComponent]
}).compileComponents();
fixture = TestBed.createComponent(WelcomeComponent);
component = fixture.componentInstance;
});
it('should match snapshot with default props', () => {
fixture.detectChanges();
expect(fixture.nativeElement).toMatchSnapshot();
});
it('should match snapshot with custom name', () => {
component.name = 'Alice';
fixture.detectChanges();
expect(fixture.nativeElement).toMatchSnapshot();
});
it('should match snapshot with details shown', () => {
component.name = 'Bob';
component.id = 'B12345';
component.status = 'Active';
component.showDetails = true;
fixture.detectChanges();
expect(fixture.nativeElement).toMatchSnapshot();
});
});
The first time these tests run, Jest will create snapshot files. On subsequent runs, it will compare the current output against the stored snapshots and fail if they don't match.
Mocking Dependencies
Jest makes it easy to mock dependencies. Let's see an example with a component that uses a service:
// user.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
export interface User {
id: number;
name: string;
email: string;
}
@Injectable({
providedIn: 'root'
})
export class UserService {
private apiUrl = 'https://api.example.com/users';
constructor(private http: HttpClient) {}
getUsers(): Observable<User[]> {
return this.http.get<User[]>(this.apiUrl);
}
}
// user-list.component.ts
import { Component, OnInit } from '@angular/core';
import { User, UserService } from './user.service';
@Component({
selector: 'app-user-list',
template: `
<div *ngIf="loading">Loading users...</div>
<div *ngIf="error">{{ error }}</div>
<ul *ngIf="!loading && !error">
<li *ngFor="let user of users">
{{ user.name }} ({{ user.email }})
</li>
</ul>
`
})
export class UserListComponent implements OnInit {
users: User[] = [];
loading = true;
error = '';
constructor(private userService: UserService) {}
ngOnInit(): void {
this.userService.getUsers().subscribe(
users => {
this.users = users;
this.loading = false;
},
err => {
this.error = 'Failed to load users';
this.loading = false;
}
);
}
}
Here's how to test this component with mocked dependencies:
// user-list.component.spec.ts
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { of, throwError } from 'rxjs';
import { UserListComponent } from './user-list.component';
import { UserService } from './user.service';
describe('UserListComponent', () => {
let component: UserListComponent;
let fixture: ComponentFixture<UserListComponent>;
let userServiceMock: jest.Mocked<UserService>;
beforeEach(async () => {
userServiceMock = {
getUsers: jest.fn()
} as any;
await TestBed.configureTestingModule({
declarations: [UserListComponent],
providers: [
{ provide: UserService, useValue: userServiceMock }
]
}).compileComponents();
fixture = TestBed.createComponent(UserListComponent);
component = fixture.componentInstance;
});
it('should load users successfully', () => {
const mockUsers = [
{ id: 1, name: 'John Doe', email: '[email protected]' },
{ id: 2, name: 'Jane Smith', email: '[email protected]' }
];
userServiceMock.getUsers.mockReturnValue(of(mockUsers));
fixture.detectChanges(); // triggers ngOnInit
expect(component.users).toEqual(mockUsers);
expect(component.loading).toBeFalsy();
expect(component.error).toBe('');
const compiled = fixture.nativeElement;
const items = compiled.querySelectorAll('li');
expect(items.length).toBe(2);
expect(items[0].textContent).toContain('John Doe');
expect(items[1].textContent).toContain('Jane Smith');
});
it('should handle error when loading users fails', () => {
userServiceMock.getUsers.mockReturnValue(throwError('API error'));
fixture.detectChanges(); // triggers ngOnInit
expect(component.users).toEqual([]);
expect(component.loading).toBeFalsy();
expect(component.error).toBe('Failed to load users');
const compiled = fixture.nativeElement;
expect(compiled.textContent).toContain('Failed to load users');
});
});
Running Tests in Watch Mode
One of Jest's best features is its interactive watch mode. To use it, run:
npm run test:watch
This will start Jest in watch mode, which monitors file changes and re-runs tests related to changed files automatically. The watch mode interface also allows you to filter tests, run specific tests, and more.
Jest Code Coverage
Jest includes built-in code coverage reporting. To generate a coverage report, run:
npm run test:coverage
This will generate a detailed coverage report in the /coverage
directory, showing which parts of your code have test coverage.
Common Jest Matchers for Angular Testing
Jest provides many matchers for assertions. Here are some commonly used ones:
expect(value).toBe(expected)
- Compares primitive values or object referencesexpect(value).toEqual(expected)
- Deep equality check for objectsexpect(value).toBeTruthy()
- Checks if value is truthyexpect(value).toBeFalsy()
- Checks if value is falsyexpect(mockFn).toHaveBeenCalled()
- Verifies a mock function was calledexpect(mockFn).toHaveBeenCalledWith(arg1, arg2)
- Verifies call argumentsexpect(element).toMatchSnapshot()
- Compares with a stored snapshotexpect(value).toBeInstanceOf(Class)
- Checks if value is an instance of Class
Testing HTTP Requests with Jest and Angular's HttpClientTestingModule
Testing components that make HTTP requests requires proper mocking. Angular provides HttpClientTestingModule
for this purpose:
// api.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class ApiService {
constructor(private http: HttpClient) {}
getData(): Observable<any[]> {
return this.http.get<any[]>('https://api.example.com/data');
}
postData(data: any): Observable<any> {
return this.http.post<any>('https://api.example.com/data', data);
}
}
Here's how to test it:
// api.service.spec.ts
import { TestBed } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { ApiService } from './api.service';
describe('ApiService', () => {
let service: ApiService;
let httpMock: HttpTestingController;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [ApiService]
});
service = TestBed.inject(ApiService);
httpMock = TestBed.inject(HttpTestingController);
});
afterEach(() => {
httpMock.verify(); // Ensure no outstanding requests
});
it('should retrieve data via GET', () => {
const dummyData = [
{ id: 1, name: 'Item 1' },
{ id: 2, name: 'Item 2' }
];
service.getData().subscribe(data => {
expect(data.length).toBe(2);
expect(data).toEqual(dummyData);
});
// Expect a call to this URL
const req = httpMock.expectOne('https://api.example.com/data');
// Assert that the request is a GET
expect(req.request.method).toBe('GET');
// Respond with mock data
req.flush(dummyData);
});
it('should send data via POST', () => {
const payload = { name: 'New Item', value: 123 };
const dummyResponse = { id: 3, ...payload };
service.postData(payload).subscribe(response => {
expect(response).toEqual(dummyResponse);
});
// Expect a call to this URL with the right data
const req = httpMock.expectOne('https://api.example.com/data');
expect(req.request.method).toBe('POST');
expect(req.request.body).toEqual(payload);
// Respond with mock data
req.flush(dummyResponse);
});
});
Summary
In this guide, we've covered how to integrate Jest with Angular applications for more efficient and feature-rich testing. We've explored:
- Setting up Jest in an Angular project
- Writing basic tests for components and services
- Using snapshot testing for UI components
- Mocking dependencies with Jest
- Testing HTTP requests
- Running tests in watch mode and generating coverage reports
Jest offers a powerful yet simple testing experience that can significantly improve your Angular development workflow. With its fast execution, built-in mocking capabilities, and excellent developer experience, it's a compelling alternative to Angular's default testing setup.
Additional Resources
Exercises
- Convert an existing Angular project from Karma/Jasmine to Jest.
- Write Jest tests for a component that uses forms (both template-driven and reactive forms).
- Create a service with HTTP dependencies and test it using Jest mocks.
- Implement snapshot testing for a set of UI components.
- Set up a CI pipeline that runs Jest tests and reports coverage.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)