Developing with a Stub backend on Angular 2

In Angular 1 I always use during development a stub or mock REST API backend that allows me running the application without the real API. In this way, the front-end implementation can be started before or in parallel with the backend. It is also useful for small prototypes or demos.

You could quickly implement a fake backend using Node or something similar, but I recommend to mock the backend directly in Angular. It is faster because the application won’t hit the wire and what I really like is that it is stateless (every time the browser is reloaded the stub data is reinitialized).

Those two points are great for local development but what I find fundamental is to use it for running protractor E2E tests, that way I always can rely on a backend substitute that does not keep state between test (protractor reloads the browser before every test, and again the stub data gets reinitialized).

This post shows how to configure a stub backend on Angular 2 using the MockBackend test class.
On develop mode we will override the Http provider to, instead of using a real backend implementation such as XHRBackend, use the MockBackend.

I define the stub backend provider in a separate file stub-backend-provider.ts, here is all the magic happens swapping the backend provider for develop mode and defining stub responses for determined HTTP requests.

src/app/stub-backed/stub-backend-provider.ts

import { Http, BaseRequestOptions, Response, ResponseOptions, RequestMethod, XHRBackend } from '@angular/http';
import { MockBackend, MockConnection } from '@angular/http/testing';

import { environment } from '../../environments/environment';
import { Task } from '../tasks-list/task';
import { generateUuid, getUuidFromUrl } from './stub-backend-utils';

/**
 * Provider to allow the use of a stub backend instead of a real Http service for backend-less development.
 */
export let stubBackendProvider = {
  provide: Http,
  deps: [MockBackend, BaseRequestOptions, XHRBackend],
  useFactory: (mockBackend: MockBackend, options: BaseRequestOptions, realBackend: XHRBackend) => {

    if (!environment.stubBackend) {
      console.log('Configuring real Http backend...');
      return new Http(realBackend, options);
    }

    console.log('Configuring stub Http backend...');

    let tasks: Task[] = [
      {id: '9509c8b4-ad34-4378-b49c-c9206dfd7f75', name: 'Buy milk', done: false, userId: 'user-1'},
      {id: '1b35d8f8-9e80-4316-b3e3-135a8f81200f', name: 'Pay rent', done: true, userId: 'user-1'}];

    mockBackend.connections.subscribe((connection: MockConnection) => {

      // wrap in timeout to simulate server api call
      setTimeout(() => {

        // Get all tasks
        if (connection.request.method === RequestMethod.Get && connection.request.url.match('/tasks$')) {
          connection.mockRespond(new Response(new ResponseOptions({body: tasks.slice()})));
          return;
        }

        // Save task
        if (connection.request.method === RequestMethod.Post && connection.request.url.match('/tasks$')) {
          let newTask = JSON.parse(connection.request.getBody());
          newTask.id = generateUuid();
          tasks.push(newTask);

          connection.mockRespond(new Response(new ResponseOptions({body: newTask})));
          return;
        }

        // Delete task
        if (connection.request.method === RequestMethod.Delete && connection.request.url.match('/tasks/*')) {
          let id = getUuidFromUrl(connection.request.url);
          tasks = tasks.filter(task => task.id !== id);

          connection.mockRespond(new Response(new ResponseOptions()));
          return;
        }
      }, 500);

    });

    return new Http(mockBackend, options);
  }
};

The provider requires uses a new Angular CLI environment property that I called stubBackend, with a value of true on develop and false on production.
/src/environments/environment.ts

export const environment = {
  production: false,
  stubBackend: true
};

/src/environments/environment.prod.ts

export const environment = {
  production: false,
  stubBackend: false
};

Continue reading “Developing with a Stub backend on Angular 2”

Unit test Http Services on Angular 2

Angular 2 testing documentation is more than good, but I believe that it is missing a crucial bit. How should we test services that make use of Http?

For example, let’s say that we want to unit test a TasksService with a getTasks method to GET task resources from a REST API.

import { Injectable } from '@angular/core';
import { Http } from '@angular/http';
import 'rxjs/add/operator/toPromise';
import { Task } from './task';

@Injectable()
export class TasksService {
  private tasksUrl = 'http://a-task-list-api.com/tasks';

  constructor(private http: Http) {}

  getTasks(): Promise<Task[]> {
    return this.http.get(this.tasksUrl)
      .toPromise()
      .then(response => response.json() as Task[]);
  }
}

The service uses angular Http service to perform HTTP requests. Unit test of this service should be done in isolation because we donĀ“t want our service to be hitting the real network and at the same time, we want to control and fake the responses from the API. For that reason, we need to use a mock Http service.

Implementing a fake version of the Http service can get really complicated, the good thing is that Angular 2 already provides one for us, the MockBackend.

This is a full example of unit test using MockBackend and BaseRequestOptions to create a Http mock.

import { TestBed, inject, tick, fakeAsync } from '@angular/core/testing';
import { BaseRequestOptions, Http, ConnectionBackend, Response, ResponseOptions, RequestMethod } from '@angular/http';
import { MockBackend } from '@angular/http/testing';

import { TasksService } from './tasks.service';
import { Task } from './task';

describe('TasksService', () => {

  const TASKS: Task[] = [
    {id: 'task-1', name: 'Buy milk', done: false, userId: 'user-1'},
    {id: 'task-2', name: 'Pay rent', done: true, userId: 'user-1'}
  ];

  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [
        {
          provide: Http,
          deps: [MockBackend, BaseRequestOptions]
          useFactory: (backend: ConnectionBackend, defaultOptions: BaseRequestOptions) => {
          return new Http(backend, defaultOptions);
         }
        },
        {provide: MockBackend, useClass: MockBackend},
        {provide: BaseRequestOptions, useClass: BaseRequestOptions},
        {provide: TasksService, useClass: TasksService}
      ]
    });
  });

  describe('getTasks()', () => {

    it('should return all tasks', inject([TasksService, MockBackend], fakeAsync((tasksService: TasksService, mockBackend: MockBackend) => {
      let result;

      mockBackend.connections.subscribe(c => {
        expect(c.request.method).toBe(RequestMethod.Get);
        expect(c.request.url).toBe('http://paucls-task-list-api.herokuapp.com/tasks');
        let response = new ResponseOptions({body: TASKS});
        c.mockRespond(new Response(response));
      });

      tasksService.getTasks().then(tasks => {
        result = tasks;
      });
      tick();

      expect(result.length).toBe(TASKS.length);
    })));

  });

});

As a reference, in Angular 1 we use the $httpBackend service mock.

    describe('getTasks()', function () {
        it('should return all tasks', function () {
            $httpBackend.expectGET('/tasks').respond(TEST_TASKS);

            let tasks = TasksService.getTasks();
            $httpBackend.flush();
            tasks = resolvePromise(tasks, $q, $scope);

            expect(tasks).toEqual(TEST_TASKS);
        });
    });

Documentation:
https://angular.io/docs/ts/latest/guide/testing.html#!#isolated-service-tests
http://gist.asciidoctor.org/?github-mraible%2Fng2-demo%2F%2FREADME.adoc
https://semaphoreci.com/community/tutorials/testing-angular-2-http-services-with-jasmine