As for any external dependency, you must mock the HTTP backend so your tests can simulate interaction with a remote server. The @angular/common/http/testing
library makes it straightforward to set up such mocking.
Angular's HTTP testing library is designed for a pattern of testing in which the app executes code and makes requests first. The test then expects that certain requests have or have not been made, performs assertions against those requests, and finally provides responses by "flushing" each expected request.
At the end, tests can verify that the app made no unexpected requests.
You can run these sample tests in a live coding environment.
The tests described in this guide are in
src/testing/http-client.spec.ts
. There are also tests of an application data service that callHttpClient
insrc/app/heroes/heroes.service.spec.ts
.
To begin testing calls to HttpClient
, import the HttpClientTestingModule
and the mocking controller, HttpTestingController
, along with the other symbols your tests require.
// Http testing module and mocking controller import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; // Other imports import { TestBed } from '@angular/core/testing'; import { HttpClient, HttpErrorResponse } from '@angular/common/http';
Then add the HttpClientTestingModule
to the TestBed
and continue with the setup of the service-under-test.
describe('HttpClient testing', () => { let httpClient: HttpClient; let httpTestingController: HttpTestingController; beforeEach(() => { TestBed.configureTestingModule({ imports: [ HttpClientTestingModule ] }); // Inject the http service and test controller for each test httpClient = TestBed.inject(HttpClient); httpTestingController = TestBed.inject(HttpTestingController); }); /// Tests begin /// });
Now requests made in the course of your tests hit the testing backend instead of the normal backend.
This setup also calls TestBed.inject()
to inject the HttpClient
service and the mocking controller so they can be referenced during the tests.
Now you can write a test that expects a GET Request to occur and provides a mock response.
it('can test HttpClient.get', () => { const testData: Data = {name: 'Test Data'}; // Make an HTTP GET request httpClient.get<Data>(testUrl) .subscribe(data => // When observable resolves, result should match test data expect(data).toEqual(testData) ); // The following `expectOne()` will match the request's URL. // If no requests or multiple requests matched that URL // `expectOne()` would throw. const req = httpTestingController.expectOne('/data'); // Assert that the request is a GET. expect(req.request.method).toEqual('GET'); // Respond with mock data, causing Observable to resolve. // Subscribe callback asserts that correct data was returned. req.flush(testData); // Finally, assert that there are no outstanding requests. httpTestingController.verify(); });
The last step, verifying that no requests remain outstanding, is common enough for you to move it into an afterEach()
step:
afterEach(() => { // After every test, assert that there are no more pending requests. httpTestingController.verify(); });
If matching by URL isn't sufficient, it's possible to implement your own matching function. For example, you could look for an outgoing request that has an authorization header:
// Expect one request with an authorization header const req = httpTestingController.expectOne( request => request.headers.has('Authorization') );
As with the previous expectOne()
, the test fails if 0 or 2+ requests satisfy this predicate.
If you need to respond to duplicate requests in your test, use the match()
API instead of expectOne()
. It takes the same arguments but returns an array of matching requests. Once returned, these requests are removed from future matching and you are responsible for flushing and verifying them.
// get all pending requests that match the given URL const requests = httpTestingController.match(testUrl); expect(requests.length).toEqual(3); // Respond to each request with different results requests[0].flush([]); requests[1].flush([testData[0]]); requests[2].flush(testData);
You should test the app's defenses against HTTP requests that fail.
Call request.flush()
with an error message, as seen in the following example.
it('can test for 404 error', () => { const emsg = 'deliberate 404 error'; httpClient.get<Data[]>(testUrl).subscribe({ next: () => fail('should have failed with the 404 error'), error: (error: HttpErrorResponse) => { expect(error.status).withContext('status').toEqual(404); expect(error.error).withContext('message').toEqual(emsg); }, }); const req = httpTestingController.expectOne(testUrl); // Respond with mock error req.flush(emsg, { status: 404, statusText: 'Not Found' }); });
Alternatively, call request.error()
with a ProgressEvent
.
it('can test for network error', done => { // Create mock ProgressEvent with type `error`, raised when something goes wrong // at network level. e.g. Connection timeout, DNS error, offline, etc. const mockError = new ProgressEvent('error'); httpClient.get<Data[]>(testUrl).subscribe({ next: () => fail('should have failed with the network error'), error: (error: HttpErrorResponse) => { expect(error.error).toBe(mockError); done(); }, }); const req = httpTestingController.expectOne(testUrl); // Respond with mock error req.error(mockError); });
© 2010–2023 Google, Inc.
Licensed under the Creative Commons Attribution License 4.0.
https://angular.io/guide/http-test-requests