Skip to content
Angular ng http 4 min read

Testing HTTP Requests

Services that talk to a backend are some of the most important code to test, yet you never want a unit test to hit a real network. Angular ships a dedicated testing module that swaps the real HTTP backend for a controllable fake, letting you assert which requests were made, inspect their headers and bodies, and decide exactly what each one returns. This page shows how to wire up provideHttpClientTesting and drive your tests with HttpTestingController.

Setting up the testing module

The testing utilities live in @angular/common/http/testing. You provide the real HttpClient via provideHttpClient() and then override its backend with provideHttpClientTesting(). The order matters: the testing provider must come after the real one so it can replace the live HttpHandler with a mock.

Grab the HttpTestingController from the TestBed to interact with outgoing requests.

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

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

  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [
        UserService,
        provideHttpClient(),
        provideHttpClientTesting(),
      ],
    });

    service = TestBed.inject(UserService);
    httpMock = TestBed.inject(HttpTestingController);
  });

  afterEach(() => {
    // Fails the test if any unexpected requests were made.
    httpMock.verify();
  });
});

The afterEach call to verify() is what makes these tests strict: if your service fired a request you never expected, or never matched, the test fails. This catches accidental duplicate calls and typos in URLs.

The service under test

Here is a small standalone-friendly service that we will test throughout. It uses inject() and returns observables.

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

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

@Injectable({ providedIn: 'root' })
export class UserService {
  private http = inject(HttpClient);
  private base = '/api/users';

  getUsers(): Observable<User[]> {
    return this.http.get<User[]>(this.base);
  }

  create(name: string): Observable<User> {
    return this.http.post<User>(this.base, { name });
  }
}

Expecting and flushing a request

The core workflow is: call the service method, subscribe to assert on the result, use expectOne() to capture the pending request, then call flush() to deliver a fake response. Because HttpClient is cold, the request is not actually dispatched until something subscribes.

it('fetches users', () => {
  const mockUsers: User[] = [
    { id: 1, name: 'Ada' },
    { id: 2, name: 'Linus' },
  ];

  let result: User[] | undefined;
  service.getUsers().subscribe((users) => (result = users));

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

  req.flush(mockUsers); // resolve the observable with this body

  expect(result).toEqual(mockUsers);
});

The subscription callback runs synchronously the moment you call flush(), so the assertion after it sees the populated result.

Always assert on req.request.method. A get and a post to the same URL both match a bare URL string, so verifying the verb prevents a silent mismatch from passing.

Matching requests

expectOne accepts a URL string, or a predicate function for finer control over method, params, or body. There is also expectNone and a match() helper for multiple requests.

MethodPurpose
expectOne(url)Exactly one request matched; returns it
expectOne(req => boolean)Match on method, URL, params, or body
expectNone(url)Assert no request matched
match(predicate)Return an array of all matching requests
verify()Assert no outstanding requests remain
it('posts a new user with the right body', () => {
  service.create('Grace').subscribe();

  const req = httpMock.expectOne(
    (r) => r.method === 'POST' && r.url === '/api/users'
  );
  expect(req.request.body).toEqual({ name: 'Grace' });

  req.flush({ id: 3, name: 'Grace' });
});

Testing error responses

Real APIs fail, and your error paths deserve coverage too. Pass an error body and a status object to flush(), or use the lower-level error() method with a ProgressEvent to simulate network failures.

it('surfaces a 500 as an HttpErrorResponse', () => {
  let status: number | undefined;

  service.getUsers().subscribe({
    next: () => fail('expected an error'),
    error: (err) => (status = err.status),
  });

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

  expect(status).toBe(500);
});

Output:

UserService
  ✓ fetches users
  ✓ posts a new user with the right body
  ✓ surfaces a 500 as an HttpErrorResponse

3 specs, 0 failures

Testing interceptors

Functional interceptors are tested the same way, because they sit between your service and the mock backend. Register them with withInterceptors() and assert on the request the interceptor produced.

import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { authInterceptor } from './auth.interceptor';

TestBed.configureTestingModule({
  providers: [
    UserService,
    provideHttpClient(withInterceptors([authInterceptor])),
    provideHttpClientTesting(),
  ],
});

it('attaches the auth header via the interceptor', () => {
  service.getUsers().subscribe();

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

  req.flush([]);
});

Best Practices

  • Always call httpMock.verify() in afterEach so stray or duplicate requests fail loudly.
  • Subscribe to the observable before calling expectOne; HttpClient is lazy and dispatches nothing until subscription.
  • Assert on req.request.method, body, params, and headers, not just the URL.
  • Keep provideHttpClientTesting() after provideHttpClient() in the providers array so the mock backend wins.
  • Cover both the happy path and error paths by flushing non-2xx statuses.
  • Prefer predicate matchers over raw URL strings when query params or methods distinguish requests.
  • Test interceptors through the real withInterceptors() pipeline rather than mocking them away.
Last updated June 14, 2026
Was this helpful?