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:
// 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:
// 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:
it('should retrieve all users', () => {
  const mockUsers: User[] = [
    { id: 1, name: 'John Doe', email: '[email protected]' },
    { id: 2, name: 'Jane Smith', email: '[email protected]' }
  ];
  // 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:
it('should retrieve a single user by id', () => {
  const mockUser: User = { id: 1, name: 'John Doe', email: '[email protected]' };
  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:
it('should add a new user', () => {
  const newUser: User = { id: 3, name: 'Bob Johnson', email: '[email protected]' };
  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:
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:
it('should handle multiple requests', () => {
  const mockUsers: User[] = [
    { id: 1, name: 'John Doe', email: '[email protected]' },
    { id: 2, name: 'Jane Smith', email: '[email protected]' }
  ];
  // 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:
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:
// 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:
// 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
- 
Always verify your test expectations: Use httpTestingController.verify()after each test to ensure there are no outstanding requests.
- 
Test error scenarios: Make sure your services handle HTTP errors appropriately. 
- 
Test the correct HTTP method: Verify that your service uses the correct HTTP method (GET, POST, PUT, DELETE, etc.). 
- 
Test headers and parameters: Verify that your service sends the correct headers and URL parameters. 
- 
Use realistic mock data: Your mock responses should reflect what the real API would return. 
- 
Isolate HTTP tests: Test HTTP requests in isolation from other parts of your application. 
- 
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:
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:
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 HttpClientTestingModuleandHttpTestingController
- 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
- 
Create a service that performs CRUD operations (Create, Read, Update, Delete) on a resource of your choice, and write comprehensive tests for each operation. 
- 
Extend the pagination example to include sorting functionality and write tests to verify it works correctly. 
- 
Implement a service that handles file uploads and write tests for it. 
- 
Create a service with retry logic for failed requests and test that it correctly retries the specified number of times. 
- 
Implement authentication-related HTTP services (login, logout, token refresh) and write tests to verify their behavior. 
💡 Found a typo or mistake? Click "Edit this page" to suggest a correction. Your feedback is greatly appreciated!