Skip to main content

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:

bash
npm install --save-dev jest @types/jest jest-preset-angular

Step 2: Configure Jest

Create a jest.config.js file in your project root:

javascript
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:

typescript
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:

json
{
"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:

json
{
"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:

typescript
// 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:

typescript
// 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:

typescript
// 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:

typescript
// 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.

typescript
// 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:

typescript
// 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:

typescript
// 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);
}
}
typescript
// 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:

typescript
// 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:

bash
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:

bash
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 references
  • expect(value).toEqual(expected) - Deep equality check for objects
  • expect(value).toBeTruthy() - Checks if value is truthy
  • expect(value).toBeFalsy() - Checks if value is falsy
  • expect(mockFn).toHaveBeenCalled() - Verifies a mock function was called
  • expect(mockFn).toHaveBeenCalledWith(arg1, arg2) - Verifies call arguments
  • expect(element).toMatchSnapshot() - Compares with a stored snapshot
  • expect(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:

typescript
// 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:

typescript
// 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

  1. Convert an existing Angular project from Karma/Jasmine to Jest.
  2. Write Jest tests for a component that uses forms (both template-driven and reactive forms).
  3. Create a service with HTTP dependencies and test it using Jest mocks.
  4. Implement snapshot testing for a set of UI components.
  5. 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! :)