Angular2: hard time unit testing Http requests
To follow up on my article about TypeScript generics in Angular2, I would like to unit test my Stripe client.
It involves mocking the Angular2 Http service, and it's far more complicated than unit testing the Router service. I first tried to inject a mock of the Http service and return custom Observable responses but this led to strange errors and cumbersome code.
I quickly switched to the recommended way, using MockBackend.
The service to test¶
As a remainder, the service tested is a Stripe client. It makes recursive Http request to Stripe API in order to fetch customers and plans:
import {Injectable} from 'angular2/core';
import {Http, Headers, URLSearchParams} from 'angular2/http';
import {Customer} from './Customer';
import {Plan} from './Plan';
import {PaginatedList} from './PaginatedList';
import {HasId} from './HasId';
const stripeUrl: string = 'https://api.stripe.com/v1';
@Injectable()
export class StripeClient {
private apiKey: string = '';
constructor(private http: Http) {
}
getApiKey(): string {
return this.apiKey;
}
setApiKey(apiKey: string) {
this.apiKey = apiKey;
}
getCustomers(callback: (customers: PaginatedList<Customer>)=>boolean) {
this.getData('/customers', null, callback);
}
getPlans(callback: (plans: PaginatedList<Plan>)=>boolean) {
this.getData('/plans', null, callback);
}
private getData<T extends HasId> (endpoint: string, starting_after: string, callback: (data: PaginatedList<T>)=>boolean) {
const params: URLSearchParams = new URLSearchParams();
params.set('limit', '100');
if (starting_after){
params.set('starting_after', starting_after);
}
this.http.get(stripeUrl + endpoint, {headers: this.getAuthHeaders(), search: params})
.map(res => res.json())
.subscribe((paginatedList: PaginatedList<T>) => {
if (callback(paginatedList) && paginatedList.has_more){
this.getData(endpoint, paginatedList.data[paginatedList.data.length - 1].id, callback);
}
});
}
private getAuthHeaders(): Headers {
var headers = new Headers();
headers.append('Authorization', 'Bearer ' + this.apiKey);
return headers;
}
}
The unit test¶
The idea is to replace the standard XHRBackend my the mock one: MockBackend.
This mock backend provides methods and fields to handle incoming requests:
import {
it,
inject,
injectAsync,
beforeEachProviders
} from 'angular2/testing';
import {HTTP_PROVIDERS, XHRBackend, Response, ResponseOptions} from 'angular2/http';
import {provide} from 'angular2/core';
import {MockBackend} from 'angular2/http/testing';
import {MockConnection} from 'angular2/src/http/backends/mock_backend';
import {Plan} from './Plan';
import {PaginatedList} from './PaginatedList';
import {StripeClient} from './StripeClient';
import 'rxjs/Rx';
describe('StripeClient', () => {
beforeEachProviders(() => [
HTTP_PROVIDERS,
provide(XHRBackend, {useClass: MockBackend}),
StripeClient
]);
it('should set and get secret key', inject([StripeClient], (client) => {
client.setApiKey('test');
expect(client.getApiKey()).toEqual('test');
}));
it('should get plans', inject([XHRBackend, StripeClient], (mockBackend, client) => {
mockBackend.connections.subscribe(
(connection: MockConnection) => {
connection.mockRespond(new Response(
new ResponseOptions({
body: JSON.stringify(
{
has_more: false,
data: [{
name:'name',
id:'id'
}]
}
)
}
)));
});
var plans: Plan[] = [];
var has_more: boolean;
client.getPlans((_plans: PaginatedList<Plan>) => {
plans = plans.concat(_plans.data);
has_more = _plans.has_more;
});
expect(plans.length).toBe(1);
expect(has_more).toBe(false);
}));
});
This test is far from perfect.
First of all I have to import some Rx stuff (import 'rxjs/Rx';
) to avoid the following error:
Failed: undefined is not a function (evaluating 'this.http.get(stripeUrl+endpoint,{headers:this.getAuthHeaders(),search:params}).map(function(res){__cov_7Fe_TqajXZVQEOBsQl5hYg.f['10']++;__cov_7Fe_TqajXZVQEOBsQl5hYg.s['34']++;return res.json();})')
The map
method from Observable is not recognized without this import.
Then I could not find a way to mock the successive calls made against Stripe API. I cannot subscribe to multiple urls by doing several calls to mockBackend.connections.subscribe
.
It's still a beta version / work in progress and we are far from the ease of use offered by AngularJS $httpBackend mock.