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. Agetand apostto 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.
| Method | Purpose |
|---|---|
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()inafterEachso stray or duplicate requests fail loudly. - Subscribe to the observable before calling
expectOne;HttpClientis lazy and dispatches nothing until subscription. - Assert on
req.request.method,body,params, and headers, not just the URL. - Keep
provideHttpClientTesting()afterprovideHttpClient()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.