Skip to main content

Angular HTTP Testing

Introduction

When building Angular applications, communication with backend services via HTTP is a common requirement. Testing these HTTP interactions is crucial to ensure your application correctly handles API requests and responses, manages errors, and behaves as expected in various network conditions.

In this tutorial, we'll explore how to effectively test HTTP requests in Angular applications using the built-in testing utilities provided by the framework. By the end, you'll understand how to write robust tests for your HTTP services that verify both successful operations and error handling.

Prerequisites

Before diving into HTTP testing, you should have:

  • Basic understanding of Angular components and services
  • Familiarity with Angular's dependency injection system
  • Knowledge of basic testing concepts in Angular using Jasmine and Karma
  • Understanding of Observables in RxJS

Setting Up the Testing Environment

Angular provides a powerful HttpClientTestingModule specifically designed for testing HTTP requests. This module includes the HttpTestingController, which allows you to mock HTTP requests and provide test responses without making actual network calls.

First, let's set up a basic service that makes HTTP requests:

typescript
// data.service.ts
import { Injectable } from '@angular/core';
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';

export interface User {
id: number;
name: string;
email: string;
}

@Injectable({
providedIn: 'root'
})
export class DataService {
private apiUrl = 'https://api.example.com/users';

constructor(private http: HttpClient) { }

getUsers(): Observable<User[]> {
return this.http.get<User[]>(this.apiUrl)
.pipe(
catchError(this.handleError)
);
}

getUser(id: number): Observable<User> {
return this.http.get<User>(`${this.apiUrl}/${id}`)
.pipe(
catchError(this.handleError)
);
}

addUser(user: User): Observable<User> {
return this.http.post<User>(this.apiUrl, user)
.pipe(
catchError(this.handleError)
);
}

private handleError(error: HttpErrorResponse) {
if (error.error instanceof ErrorEvent) {
// Client-side error
console.error('An error occurred:', error.error.message);
} else {
// Server-side error
console.error(
`Backend returned code ${error.status}, ` +
`body was: ${error.error}`);
}
// Return an observable with a user-facing error message
return throwError('Something went wrong; please try again later.');
}
}

Now, let's create a test file for this service:

typescript
// data.service.spec.ts
import { TestBed } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { DataService, User } from './data.service';
import { HttpErrorResponse } from '@angular/common/http';

describe('DataService', () => {
let service: DataService;
let httpTestingController: HttpTestingController;
const apiUrl = 'https://api.example.com/users';

beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [DataService]
});

// Inject the service and the test controller
service = TestBed.inject(DataService);
httpTestingController = TestBed.inject(HttpTestingController);
});

afterEach(() => {
// After each test, verify that there are no more pending requests
httpTestingController.verify();
});

// Tests will be added here
});

Testing GET Requests

Let's start by writing tests for the getUsers() method:

typescript
it('should retrieve all users', () => {
const mockUsers: User[] = [
{ id: 1, name: 'John Doe', email: 'john@example.com' },
{ id: 2, name: 'Jane Smith', email: 'jane@example.com' }
];

// Make the call
service.getUsers().subscribe(users => {
// Assert that the returned data matches what we expect
expect(users).toEqual(mockUsers);
expect(users.length).toBe(2);
});

// Expect a request to this URL
const req = httpTestingController.expectOne(apiUrl);

// Assert that the request is a GET
expect(req.request.method).toEqual('GET');

// Respond with mock data
req.flush(mockUsers);
});

Now, let's test the getUser(id) method:

typescript
it('should retrieve a single user by id', () => {
const mockUser: User = { id: 1, name: 'John Doe', email: 'john@example.com' };
const userId = 1;

service.getUser(userId).subscribe(user => {
expect(user).toEqual(mockUser);
});

const req = httpTestingController.expectOne(`${apiUrl}/${userId}`);
expect(req.request.method).toEqual('GET');
req.flush(mockUser);
});

Testing POST Requests

Next, let's test our addUser() method:

typescript
it('should add a new user', () => {
const newUser: User = { id: 3, name: 'Bob Johnson', email: 'bob@example.com' };

service.addUser(newUser).subscribe(user => {
expect(user).toEqual(newUser);
});

const req = httpTestingController.expectOne(apiUrl);
expect(req.request.method).toEqual('POST');
expect(req.request.body).toEqual(newUser); // Verify the correct data was sent
req.flush(newUser);
});

Testing Error Responses

Testing how your service handles errors is just as important as testing successful responses:

typescript
it('should handle error on failed user retrieval', () => {
const errorMsg = 'Something went wrong; please try again later.';

service.getUsers().subscribe(
() => fail('Expected an error, not users'),
error => {
expect(error).toEqual(errorMsg);
}
);

const req = httpTestingController.expectOne(apiUrl);

// Respond with a 404 status to trigger the error
req.flush('Not Found', { status: 404, statusText: 'Not Found' });
});

Testing Multiple Requests

Sometimes you need to test scenarios where multiple HTTP requests are made:

typescript
it('should handle multiple requests', () => {
const mockUsers: User[] = [
{ id: 1, name: 'John Doe', email: 'john@example.com' },
{ id: 2, name: 'Jane Smith', email: 'jane@example.com' }
];

// Make the first call
service.getUsers().subscribe();

// Make the second call
service.getUsers().subscribe();

// Expect exactly two requests to this URL
const requests = httpTestingController.match(apiUrl);
expect(requests.length).toBe(2);

// Respond to both requests with the same mock data
requests[0].flush(mockUsers);
requests[1].flush(mockUsers);
});

Testing Headers and Query Parameters

You might need to verify that your service sends the correct headers or query parameters:

typescript
it('should include authorization header', () => {
// Extend our service with a method that includes headers
spyOn(service['http'], 'get').and.callThrough();

service.getUsers().subscribe();

const req = httpTestingController.expectOne(apiUrl);

// Check if the request included authentication headers (if your service adds them)
// This assumes your service adds these headers internally
expect(service['http'].get).toHaveBeenCalledWith(
apiUrl,
jasmine.objectContaining({
headers: jasmine.anything()
})
);

req.flush([]);
});

Real-World Example: Testing Pagination

Let's create a more complex example where we test a service that handles paginated data:

typescript
// pagination.service.ts
import { Injectable } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';
import { Observable } from 'rxjs';

export interface PaginatedResponse<T> {
items: T[];
total: number;
page: number;
pageSize: number;
}

export interface Product {
id: number;
name: string;
price: number;
}

@Injectable({
providedIn: 'root'
})
export class ProductService {
private apiUrl = 'https://api.example.com/products';

constructor(private http: HttpClient) { }

getProducts(page: number = 1, pageSize: number = 10): Observable<PaginatedResponse<Product>> {
const params = new HttpParams()
.set('page', page.toString())
.set('pageSize', pageSize.toString());

return this.http.get<PaginatedResponse<Product>>(this.apiUrl, { params });
}
}

And here's how to test it:

typescript
// pagination.service.spec.ts
import { TestBed } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { ProductService, PaginatedResponse, Product } from './pagination.service';

describe('ProductService', () => {
let service: ProductService;
let httpTestingController: HttpTestingController;
const apiUrl = 'https://api.example.com/products';

beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [ProductService]
});

service = TestBed.inject(ProductService);
httpTestingController = TestBed.inject(HttpTestingController);
});

afterEach(() => {
httpTestingController.verify();
});

it('should retrieve paginated products with default pagination', () => {
const mockResponse: PaginatedResponse<Product> = {
items: [
{ id: 1, name: 'Product 1', price: 100 },
{ id: 2, name: 'Product 2', price: 200 }
],
total: 20,
page: 1,
pageSize: 10
};

service.getProducts().subscribe(response => {
expect(response).toEqual(mockResponse);
expect(response.items.length).toBe(2);
expect(response.total).toBe(20);
});

const req = httpTestingController.expectOne(
req => req.url === apiUrl &&
req.params.get('page') === '1' &&
req.params.get('pageSize') === '10'
);
expect(req.request.method).toEqual('GET');
req.flush(mockResponse);
});

it('should retrieve paginated products with custom pagination', () => {
const page = 2;
const pageSize = 5;

const mockResponse: PaginatedResponse<Product> = {
items: [
{ id: 6, name: 'Product 6', price: 600 },
{ id: 7, name: 'Product 7', price: 700 },
{ id: 8, name: 'Product 8', price: 800 },
{ id: 9, name: 'Product 9', price: 900 },
{ id: 10, name: 'Product 10', price: 1000 }
],
total: 20,
page: 2,
pageSize: 5
};

service.getProducts(page, pageSize).subscribe(response => {
expect(response).toEqual(mockResponse);
expect(response.page).toBe(2);
expect(response.pageSize).toBe(5);
});

const req = httpTestingController.expectOne(
req => req.url === apiUrl &&
req.params.get('page') === '2' &&
req.params.get('pageSize') === '5'
);
expect(req.request.method).toEqual('GET');
req.flush(mockResponse);
});
});

Best Practices for HTTP Testing in Angular

  1. Always verify your test expectations: Use httpTestingController.verify() after each test to ensure there are no outstanding requests.

  2. Test error scenarios: Make sure your services handle HTTP errors appropriately.

  3. Test the correct HTTP method: Verify that your service uses the correct HTTP method (GET, POST, PUT, DELETE, etc.).

  4. Test headers and parameters: Verify that your service sends the correct headers and URL parameters.

  5. Use realistic mock data: Your mock responses should reflect what the real API would return.

  6. Isolate HTTP tests: Test HTTP requests in isolation from other parts of your application.

  7. Test both successful and failed requests: Ensure your application can handle both scenarios.

Common Issues and Troubleshooting

No provider for HttpClient

If you see this error, make sure you've imported HttpClientTestingModule in your test module:

typescript
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [YourService]
});

Unexpected requests

If httpTestingController.verify() shows unexpected requests, check if your service is making requests you're not accounting for in your tests.

Testing timing issues

Sometimes HTTP operations trigger other operations or have complex timing requirements. Use Jasmine's fakeAsync and tick functions to control time in your tests:

typescript
import { fakeAsync, tick } from '@angular/core/testing';

it('should handle delayed response', fakeAsync(() => {
let response: any;

service.getDelayedData().subscribe(data => {
response = data;
});

const req = httpTestingController.expectOne('/api/delayed');
req.flush({ message: 'Delayed response' });

// Simulate waiting for 100ms
tick(100);

expect(response).toBeDefined();
expect(response.message).toBe('Delayed response');
}));

Summary

In this tutorial, you've learned how to:

  • Set up Angular HTTP testing with HttpClientTestingModule and HttpTestingController
  • Test GET, POST and other HTTP methods
  • Verify requests have the correct URL, parameters, and headers
  • Test error handling in HTTP services
  • Test complex scenarios like pagination

By properly testing your HTTP services, you can ensure your Angular application remains robust even when backend services change or experience issues.

Additional Resources

Exercises

  1. Create a service that performs CRUD operations (Create, Read, Update, Delete) on a resource of your choice, and write comprehensive tests for each operation.

  2. Extend the pagination example to include sorting functionality and write tests to verify it works correctly.

  3. Implement a service that handles file uploads and write tests for it.

  4. Create a service with retry logic for failed requests and test that it correctly retries the specified number of times.

  5. Implement authentication-related HTTP services (login, logout, token refresh) and write tests to verify their behavior.



If you spot any mistakes on this website, please let me know at feedback@compilenrun.com. I’d greatly appreciate your feedback! :)