Skip to content
Angular2: hard time unit testing Http requests

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:

Elevate your Load Testing!
Request a Demo
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.

Want to become a super load tester?
Request a Demo