Testing an Angular 2 Service with a Mocked HTTP Interface

October 27, 2016

Angular 2 is the successor of the famous and broadly used AngularJS Javascript Framework, for building dynamic web applications. Due to the huge success and popularity of its predecessor, some client projects started using Angular 2 before its official release. This resulted in Angular 2 becoming one of the most famous platforms for web and hybrid application development. Although this framework is relatively new there are already a lot of tutorials and examples explaining how to develop an Angular 2 application. However, there is one aspect of Angular 2 that is not widely covered, but is still very important for every project: Writing tests. The command used to generate the default test cases is ng generate and it's contained in the Angular 2 CLI. However, it only generates the boilerplate code that is needed for each test case. The actual tests have to be created separately and the API is still experimental, so examples are often outdated. In example, most of the available tutorials use the beforeEachProviders method, which was removed in an earlier Angular 2 pre-release and was replaced by TestBed.configureTestingModule({providers:…}) with a completely different signature. In the following example we will test a simple ItemService which executes one HTTP GET request against a REST endpoint. The ItemService will receive all items from the GET endpoint api/items, extract and return the JSON response. The response is mapped to a list of Item objects. The content of the JSON is not relevant so without further ado, here is the service:

import {Injectable} from '@angular/core';
import {Item} from './item.model';
import {Http, Response} from '@angular/http';
import 'rxjs/add/operator/map';
import {Observable} from "rxjs";

@Injectable()
export class ItemService {
    constructor(private http: Http) {
    }
    findItems(): Observable<Array<Item>> {
        return this.http.get('api/items').map((r: Response) => r.json());
    }
}

The Angular 2 CLI command will generate two things: The skeleton for the service, and the corresponding spec file to test the service. Before executing the test we need to inject all necessary providers into the ItemService. Since we are depending on the http class, we need to inject the http provider. This would be straight forward, but the http provider itself requires a ConnectionBackend and RequestOptions. The latter is easy as there is a class called BaseRequestOptions that we can use. The former is more difficult, since we do not want to use the ConnectionBackend which is used to connect to the server. Instead, we want to mock the ConnectionBackend to verify that the http request is executed and return a mocked result. Luckily, Angular 2 contains a class for this purpose called MockBackend.

Note: Be careful with the provider definition. It is necessary that we provide it as an implementation of the ConnectionBackend, otherwise we will still receive the error from the http-Class that there is no ConnectionBackend.

Obviously the same applies for the BaseRequestOptions:

beforeEach(() => {
    TestBed.configureTestingModule({
        providers: [
            {provide: RequestOptions, useClass: BaseRequestOptions},
            {provide: ConnectionBackend, useClass: MockBackend},
            Http,
            ItemService
        ]
    });
});

Now, the good news is that we are able to inject our ItemService. The bad news is that we are not yet able to assert anything, since we need a place to write our AngularJS 1 expectGET equivalent. So we basically need to inject our MockBackend, then subscribe to the observable and finally handle this inside our GET request. Beginning with the injection part, it is not obvious how to actually write the injection. The first parameter of inject requires an array of providers to be used. The second one is a list of objects to inject, in which we are able to specify the object type to inject. In our case we want the MockBackend:

it('...', inject([ConnectionBackend, ItemService], (backend: MockBackend, service: ItemService) => {});

The MockBackend contains a member called connections, which is an Observable. We can now subscribe to this observable and receive a MockConnection. Inside this MockConnection we can get the requested URI and return a result:

backend.connections.subscribe((c: MockConnection) => {
 expect(c.request.url).toEqual('api/items');
 c.mockRespond(new Response(new ResponseOptions({body: '[{"item":"Test"}]'})));
 });

And finally the handling part: The arrangement for the new request is done and we can call our service method. To verify that the async requests are done, the next step is to call backend.verifyNoPendingRequests() and finally our test method can do any kind of assertions. Our test of the http service is complete! Summarized, our test looks like this:

import {TestBed, inject, tick} from '@angular/core/testing';
import {ItemService} from './item.service';
import {MockBackend, MockConnection} from '@angular/http/testing';
import {Response, ResponseOptions, Http, ConnectionBackend, BaseRequestOptions, RequestOptions} from '@angular/http';

describe('Service: Item', () => {
        beforeEach(() => {
            TestBed.configureTestingModule({
                providers: [
                    {provide: RequestOptions, useClass: BaseRequestOptions},
                    {provide: ConnectionBackend, useClass: MockBackend},
                    Http,
                    ItemService
                ]
            });
        });

        it('should inject the service', inject([ItemService], (service: ItemService) => {
            expect(service).toBeTruthy();
        }));

        it('should download all items from the backend', inject([ConnectionBackend, ItemService],
            (backend: MockBackend,
             service: ItemService) => {
                // Arrange
                let items = null;
                backend.connections.subscribe((c: MockConnection) => {
                    expect(c.request.url).toEqual('api/items');
                    c.mockRespond(new Response(new ResponseOptions({body: '[{"item":"Test"}]'})));
                });

                // Act
                service.findItems().subscribe((q) => {
                    items = q;
                });

                // Assert
                backend.verifyNoPendingRequests();
                expect(items).toEqual([{item:'Test'}]);
            }));
    });

Our test of the Http service is complete!

About the author: Valentin Zickner

Is working since 2016 at mimacom as a Software Engineering. He has a lot of experience with cloud technologies, in addition he is specialized to Spring, Elasticsearch and Flowable.

Comments
Join us