Mocking HTTP Calls in Angular Unit Tests: A Comprehensive Guide

In Angular applications, services often rely on HTTP requests to interact with APIs, fetching or sending data critical to the application’s functionality. Testing these services in unit tests requires isolating them from external dependencies, such as live servers, to ensure tests are fast, reliable, and repeatable. Mocking HTTP calls is a powerful technique that allows developers to simulate API responses without making real network requests. This guide provides an in-depth exploration of mocking HTTP calls in Angular unit tests, using the HttpClientTestingModule and HttpTestingController. We’ll cover setup, writing tests, handling various scenarios, and advanced techniques to help you create robust tests for your Angular services.

Why Mock HTTP Calls in Unit Tests?

Unit tests focus on verifying the behavior of individual components or services in isolation, as discussed in creating unit tests with Karma. When testing services that use HttpClient to make HTTP requests, relying on a live API introduces several challenges:

  • Network Dependency: Tests fail if the server is down or unreachable.
  • Speed: Real HTTP requests slow down test execution.
  • Inconsistency: API responses may change, making tests flaky.
  • Cost and Limits: Hitting a live API may incur costs or hit rate limits.
  • Environment Issues: Tests may require specific server configurations unavailable locally.

Mocking HTTP calls addresses these issues by simulating API responses within the test environment. Angular’s HttpClientTestingModule provides a testing harness that intercepts HTTP requests and allows you to define mock responses, ensuring tests are deterministic and independent of external systems.

Understanding Angular’s HTTP Testing Tools

Angular’s testing utilities, built around the HttpClient module, make mocking HTTP calls straightforward:

  • HttpClientTestingModule: A testing module that replaces the real HttpClientModule, intercepting HTTP requests and allowing you to mock responses.
  • HttpTestingController: A service that controls mocked HTTP requests, letting you match requests, provide responses, or simulate errors.
  • TestBed: Angular’s testing utility for configuring modules and injecting dependencies, used to set up the testing environment.

These tools integrate seamlessly with Karma and Jasmine, Angular’s default testing stack, enabling you to write expressive and maintainable tests.

Setting Up the Testing Environment

Before writing tests, ensure your Angular project is configured for unit testing. If you’re new to this, refer to creating unit tests with Karma for initial setup.

Step 1: Create a Sample Service

Let’s create a UserService that fetches user data via HTTP:

ng generate service user

Edit src/app/user.service.ts:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class UserService {
  private apiUrl = '/api/users';

  constructor(private http: HttpClient) {}

  getUsers(): Observable<{ id: number; name: string }[]> {
    return this.http.get<{ id: number; name: string }[]>(this.apiUrl);
  }

  getUser(id: number): Observable<{ id: number; name: string }> {
    return this.http.get<{ id: number; name: string }>(`${this.apiUrl}/${id}`);
  }

  createUser(user: { name: string }): Observable<{ id: number; name: string }> {
    return this.http.post<{ id: number; name: string }>(this.apiUrl, user);
  }
}

This service has methods for GET and POST requests, which we’ll test by mocking HTTP responses.

Step 2: Configure the Test Environment

The Angular CLI generates a user.service.spec.ts file. Update it to include HttpClientTestingModule:

import { TestBed } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { UserService } from './user.service';

describe('UserService', () => {
  let service: UserService;
  let httpMock: HttpTestingController;

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [HttpClientTestingModule],
      providers: [UserService]
    });
    service = TestBed.inject(UserService);
    httpMock = TestBed.inject(HttpTestingController);
  });

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

  it('should be created', () => {
    expect(service).toBeTruthy();
  });
});

Key points:

  • HttpClientTestingModule: Imported in the imports array to mock HTTP requests.
  • HttpTestingController: Injected to control and verify requests.
  • httpMock.verify(): Called in afterEach to ensure no unexpected requests were made.
  • TestBed.inject: Retrieves instances of UserService and HttpTestingController.

Run tests to verify the setup:

ng test

This starts Karma, runs the tests, and confirms the service is created.

Writing Tests with Mocked HTTP Calls

Let’s write tests for the UserService methods, mocking different HTTP scenarios.

Testing a GET Request (getUsers)

The getUsers method fetches a list of users. Here’s a test:

it('should fetch a list of users', () => {
  const mockUsers = [
    { id: 1, name: 'Alice' },
    { id: 2, name: 'Bob' }
  ];

  service.getUsers().subscribe(users => {
    expect(users).toEqual(mockUsers);
    expect(users.length).toBe(2);
  });

  const req = httpMock.expectOne('/api/users');
  expect(req.request.method).toBe('GET');
  req.flush(mockUsers);
});

How it works: 1. Define Mock Data: Create a mockUsers array to simulate the API response. 2. Subscribe to the Observable: Call service.getUsers() and verify the response in the subscribe callback. 3. Expect a Request: Use httpMock.expectOne('/api/users') to match the expected URL and return a request object. 4. Verify Method: Confirm the request uses the GET method. 5. Flush Response: Use req.flush(mockUsers) to simulate a successful response with the mock data.

Run the Test: Execute ng test to confirm the test passes.

Testing a GET Request with Parameters (getUser)

The getUser method fetches a single user by ID:

it('should fetch a single user by ID', () => {
  const mockUser = { id: 1, name: 'Alice' };

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

  const req = httpMock.expectOne('/api/users/1');
  expect(req.request.method).toBe('GET');
  req.flush(mockUser);
});

Key Differences:

  • The URL includes the ID (/api/users/1).
  • The response is a single object, not an array.
  • The test verifies that the correct user is returned for the given ID.

Testing a POST Request (createUser)

The createUser method sends a POST request:

it('should create a new user', () => {
  const newUser = { name: 'Charlie' };
  const mockResponse = { id: 3, name: 'Charlie' };

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

  const req = httpMock.expectOne('/api/users');
  expect(req.request.method).toBe('POST');
  expect(req.request.body).toEqual(newUser);
  req.flush(mockResponse);
});

Additional Checks:

  • Verify the request method is POST.
  • Check that the request body matches the input (newUser).
  • Simulate the server returning the created user with an ID.

Testing Error Handling

APIs can fail, so test how the service handles errors:

it('should handle HTTP errors', () => {
  const errorMessage = 'Server error';

  service.getUsers().subscribe({
    next: () => fail('Expected an error, not users'),
    error: (error) => {
      expect(error.status).toBe(500);
      expect(error.statusText).toBe(errorMessage);
    }
  });

  const req = httpMock.expectOne('/api/users');
  req.flush('Error', { status: 500, statusText: errorMessage });
});

How it works:

  • Use the error callback in subscribe to handle errors.
  • Simulate a 500 error with req.flush('Error', { status: 500, statusText: errorMessage }).
  • Verify the error’s status and message.

For services with custom error handling, see creating custom error handlers.

Advanced Mocking Techniques

Matching Requests with Complex URLs

If your API uses query parameters, use a function in expectOne:

it('should fetch users with query parameters', () => {
  const mockUsers = [{ id: 1, name: 'Alice' }];

  service.getUsersWithParams('active=true').subscribe(users => {
    expect(users).toEqual(mockUsers);
  });

  const req = httpMock.expectOne(req => req.url === '/api/users' && req.params.get('active') === 'true');
  expect(req.request.method).toBe('GET');
  req.flush(mockUsers);
});

Assume getUsersWithParams is:

getUsersWithParams(params: string): Observable<{ id: number; name: string }[]> {
  return this.http.get<{ id: number; name: string }[]>(`${this.apiUrl}?${params}`);
}

This test verifies query parameters are sent correctly.

Testing Multiple Requests

If a service makes multiple requests, use httpMock.expectOne repeatedly:

it('should handle multiple requests', () => {
  const mockUser1 = { id: 1, name: 'Alice' };
  const mockUser2 = { id: 2, name: 'Bob' };

  service.getUser(1).subscribe(user => expect(user).toEqual(mockUser1));
  service.getUser(2).subscribe(user => expect(user).toEqual(mockUser2));

  const req1 = httpMock.expectOne('/api/users/1');
  req1.flush(mockUser1);

  const req2 = httpMock.expectOne('/api/users/2');
  req2.flush(mockUser2);
});

Note: Ensure requests are matched in the order they’re made, as HttpTestingController processes them sequentially.

Testing HTTP Headers

If your service adds custom headers, verify them:

it('should send custom headers', () => {
  const mockUsers = [{ id: 1, name: 'Alice' }];

  service.getUsersWithHeaders().subscribe(users => expect(users).toEqual(mockUsers));

  const req = httpMock.expectOne('/api/users');
  expect(req.request.headers.get('Authorization')).toBe('Bearer token');
  req.flush(mockUsers);
});

Assume:

getUsersWithHeaders(): Observable<{ id: number; name: string }[]> {
  return this.http.get<{ id: number; name: string }[]>(this.apiUrl, {
    headers: { Authorization: 'Bearer token' }
  });
}

For more on headers, see using custom HTTP headers.

Testing Interceptors

If your application uses HTTP interceptors (e.g., for authentication), include them in the test:

import { HTTP_INTERCEPTORS } from '@angular/common/http';
import { AuthInterceptor } from './auth.interceptor';

beforeEach(() => {
  TestBed.configureTestingModule({
    imports: [HttpClientTestingModule],
    providers: [
      UserService,
      { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }
    ]
  });
});

Test the interceptor’s effect, such as adding headers, as shown above.

Debugging Mocked HTTP Tests

When tests fail, debugging is critical. For general debugging tips, see debugging unit tests. Specific to HTTP mocking:

  • Verify Request Matching: Ensure httpMock.expectOne matches the correct URL and method. Log req.url or req.method to inspect.
  • Check Order: If multiple requests are mocked, confirm they’re flushed in the correct order.
  • Inspect Response: Use console.log in the subscribe callback to verify the mocked response.
  • Ensure Cleanup: Call httpMock.verify() to catch unhandled requests.

Example:

it('should debug request', () => {
  service.getUsers().subscribe({
    next: users => console.log('Users:', users),
    error: err => console.log('Error:', err)
  });

  const req = httpMock.expectOne('/api/users');
  console.log('Request URL:', req.url);
  req.flush([]);
});

Integrating Mocked Tests into Your Workflow

To make HTTP mocking a seamless part of development:

  • Run Tests Frequently: Use ng test to catch issues early.
  • Automate in CI: Add ng test --browsers=ChromeHeadless --watch=false to CI pipelines.
  • Organize Tests: Group related tests in describe blocks (e.g., GET Requests, Error Handling).
  • Reuse Mocks: Store common mock data in cypress/fixtures or a shared file for consistency.
  • Test Components: Use mocked services in component tests, as shown in [mocking services in unit tests](/angular/testing/mock-services-in-unit-tests).

FAQ

Why should I mock HTTP calls instead of using a real API?

Mocking HTTP calls ensures tests are fast, reliable, and independent of network conditions or server availability. It also avoids costs, rate limits, and flaky tests caused by changing API responses.

What’s the difference between mocking HTTP calls in unit tests and E2E tests?

Unit tests mock HTTP calls to isolate services or components, focusing on their logic. E2E tests, like those in creating E2E tests with Cypress, mock calls to simulate end-to-end workflows without hitting a live server.

How do I test services with complex HTTP requests?

Use httpMock.expectOne with a function to match URLs, parameters, or headers. For multiple requests, handle them sequentially. Test headers or interceptors by including them in TestBed. See using interceptors for HTTP.

What if my tests fail due to HTTP mocking issues?

Debug by logging request details (URL, method, body) and verifying response data. Ensure httpMock.verify() is called to catch unhandled requests. For more debugging tips, see debugging unit tests.

Conclusion

Mocking HTTP calls in Angular unit tests is a critical skill for building reliable and maintainable applications. By using HttpClientTestingModule and HttpTestingController, you can simulate API responses, test various scenarios, and ensure your services handle data and errors correctly. From basic GET and POST requests to advanced cases like query parameters, headers, and interceptors, this guide equips you with the tools to write robust tests. Integrate these practices into your workflow to catch issues early, improve code quality, and deliver exceptional Angular applications.