In this post I will show how to mock http requests in unit tests using HttpClient.
HttpClient is a huge improvement over the original Http service when it comes to mocking.
Best of all, we no longer have to define complicated provider overrides, just to do simple mocking.
Instead we can use the new HttpClient test api to map mocked object responses to urls.
In the following example I will show how to take advantage of this in a relatively complicated, multi level http request series.
The code under test consists of a request for a list of countries, chained with parallel requests for cities in each country.
Service Under Test
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/forkJoin';
import 'rxjs/add/operator/mergeMap';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/catch';
import 'rxjs/add/observable/of';
@Injectable()
export class LocationService {
constructor(private http: HttpClient) {}
getLocations() {
return this.http
.get('./assets/countries.json')
.flatMap((data: any) => Observable
.forkJoin(data.countries
.map((country: string) => this.http
.get(`./assets/${country}.json`)
.map((locations: any) => {return {country: country.toUpperCase(),
cities: locations.cities}})))
).catch(e => Observable.of({failure: e}));
}
}
As you can tell, there are several rxjs operators at play here.
How can we go about testing this relatively complicated request?
It turns out that it’s pretty straightforward with the new HttpClient api.
Below I have written tests for all the scenarios I could think of for the http call.
Unit Tests
import { TestBed } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { LocationService } from './location.service';
describe('LocationService', () => {
let locationService: LocationService;
let httpMock: HttpTestingController;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [
HttpClientTestingModule
],
providers: [
LocationService
]
});
locationService = TestBed.get(LocationService);
httpMock = TestBed.get(HttpTestingController);
});
it('should return error if country request failed', (done) => {
locationService.getLocations()
.subscribe((res: any) => {
expect(res.failure.error.type).toBe('ERROR_LOADING_COUNTRIES');
done();
});
let countryRequest = httpMock.expectOne('./assets/countries.json');
countryRequest.error(new ErrorEvent('ERROR_LOADING_COUNTRIES'));
httpMock.verify();
});
it('should return error if usa request failed', (done) => {
locationService.getLocations()
.subscribe((res: any) => {
expect(res.failure.error.type).toBe('ERROR_LOADING_COUNTRY');
done();
});
let countryRequest = httpMock.expectOne('./assets/countries.json');
countryRequest.flush({countries: ["usa", "norway"]});
let norwayRequest = httpMock.expectOne('./assets/norway.json');
norwayRequest.flush({cities: ["Oslo", "Bergen", "Trondheim"]});
let usaRequest = httpMock.expectOne('./assets/usa.json');
usaRequest.error(new ErrorEvent('ERROR_LOADING_COUNTRY'));
httpMock.verify();
});
it('should return error if norway request failed', (done) => {
locationService.getLocations()
.subscribe((res: any) => {
expect(res.failure.error.type).toBe('ERROR_LOADING_COUNTRY');
done();
});
let countryRequest = httpMock.expectOne('./assets/countries.json');
countryRequest.flush({countries: ["usa", "norway"]});
let usaRequest = httpMock.expectOne('./assets/usa.json');
usaRequest.flush({cities: ["New York", "Chicago", "Denver"]});
let norwayRequest = httpMock.expectOne('./assets/norway.json');
norwayRequest.error(new ErrorEvent('ERROR_LOADING_COUNTRY'));
httpMock.verify();
});
it('should successfully get countries and cities', (done) => {
locationService.getLocations()
.subscribe(res => {
expect(res).toEqual(
[
{country: 'USA', cities: ["New York", "Chicago", "Denver"]},
{country: 'NORWAY', cities: ["Oslo", "Bergen", "Trondheim"]}
]
);
done();
});
let countryRequest = httpMock.expectOne('./assets/countries.json');
countryRequest.flush({countries: ["usa", "norway"]});
let usaRequest = httpMock.expectOne('./assets/usa.json');
usaRequest.flush({cities: ["New York", "Chicago", "Denver"]});
let norwayRequest = httpMock.expectOne('./assets/norway.json');
norwayRequest.flush({cities: ["Oslo", "Bergen", "Trondheim"]});
httpMock.verify();
});
});
The most important part here is the request mocking.
For each request I am setting up an expect by url. Next the request is mapped to an object response using the flush method.
In the happy path scenario I am defining successful responses for every request before asserting the final object in the subscribe block.
What about error handling?
There are three error scenarios in this sample:
The initial country request can fail, but there could also be failures in the requests for cites.
In the error state tests I am testing each error by simulating request errors for each scenario.
Order actually matters when defining the request expectations here. The error case has to go last. Otherwise you will get an error saying you can’t flush a cancelled request.
In all my tests I am calling the httpMock.verify(); method. What is this doing?
This call is there to ensure that I don’t have any http calls that are unaccounted for. Basically if there are http request without an expectation, the test will fail with an error saying there are unexpected http calls.