Testing angular 2 http services with jasmine

Testing Angular 2 HTTP Services with Jasmine

Web applications send important HTTP requests to the server, so we need to test them. This tutorial will show you how to test HTTP requests in Angular 2.

Brought to you by

Semaphore

Introduction

Angular.js was designed with testability in mind, so it was easy to write tests in it. Years passed and Angular 2 came out, completely rewritten from the ground up. They followed the same guidelines, only syntax changed, but writing the tests remained easy.

It is important to write tests for our code — by doing it, we can guard against someone breaking our code by refactoring or adding new features. This might have happened to you when someone added a small feature or added equivalent code transformations, and nothing worked afterwards. Writing tests can clarify the intention of the code by giving examples for usages. It can also reveal design flaws. When a piece of code is hard to test, there might be a problem with its architecture.

We will be writing tests for an authentication service. After starting with basic tests, we will be looking at detailed assertions on the requests of the Http module.

To help you along the tutorial we will write our code in a freshly generated angular-cli project, which you can install with npm install -g angular-cli. Also, angular-cli commands will be provided to speed up development. If you want to write the examples by yourself, create a new project with ng new angular2-testing, and apply the changes at every step. File names will be in comments on top of the code snippet. The examples will be written in Typescript, which is the recommended language for writing Angular 2 applications.

Setting Up the Environment

Angular 2 depends heavily on Dependency Injection (DI) to instantiate Components, Services, Filters etc. Test frameworks are not aware of this mechanism, so we need to add wrappers around the built-in methods. If you are not familiar with Dependency Injection, there is an article about in the official documentation. Currently, wrappers only exist for Jasmine and since Mocha is not supported yet, we will be using the Jasmine test framework.

// config/karma-test-shim.js
return Promise.all([
  System.import('@angular/core/testing'),
  System.import('@angular/platform-browser-dynamic/testing')
]).then(function (providers) {
  var testing = providers[0];
  var testingBrowser = providers[1];

  // optional
  ['addProviders', 'inject', 'async'].forEach(function(functionName) {
    window[functionName] = testing[functionName];
  });

  testing.setBaseTestProviders(
    testingBrowser.TEST_BROWSER_DYNAMIC_PLATFORM_PROVIDERS,
    testingBrowser.TEST_BROWSER_DYNAMIC_APPLICATION_PROVIDERS
  );
});

From version rc4, we don't need to import Jasmine methods (describe, it, ...), they will be wrapped by default. The addProviders method stands for loading our dependencies before each test. It will set up a new Injector instance before every test with the given dependencies. Dependencies can be given as an array. It's similar to the regular bootstrapping of the Angular application, where we will set up the dependency injection. After doing that, we can use inject in our tests to automatically instantiate the dependencies from our injector.

Setting the 3 methods (addProviders, inject, async) on the global scope is optional. It only helps while writing tests by skipping the import statement for them in each test file.

To make everything work, we also need to add some basic providers (TEST_BROWSER_DYNAMIC_PLATFORM_PROVIDERS, TEST_BROWSER_DYNAMIC_APPLICATION_PROVIDERS). We can now remove imports for these methods from the beginning of each spec file and re-run the tests with ng test.

If you choose to add the methods on the global scope, you can remove the imports for them in the test files. This way, you get an error when running the tests with ng test. The Typescript compiler is complaining: Cannot find name 'inject, addProviders', because these variables are declared on the global scope, but no definition is found for them. We can fix this by adding them to the list of definitions.

// src/typings.d.ts
declare var addProviders: any;
declare var inject: any;
declare var async: any;

Writing the First Test

First, we'll write tests against the authentication service of our application. This service will allow the user to log in and it will store the authentication state. The new service can be generated with ng generate service user.

// src/app/user.service.ts
import { Injectable } from '@angular/core';

@Injectable()
export class UserService {
  private loggedIn: boolean = false;

  isLoggedIn() {
    return this.loggedIn;
  }
}

Here is the basic service that stores the login state of the user. It doesn't utilize the Injectable decorator for now, but it will come in handy when we add the Http service as a dependency.

It's time to write our first test. We'll check whether the isLoggedIn method returns false.

// src/app/user.service.spec.ts
import { addProviders, inject } from '@angular/core/testing';
import { UserService } from './user.service';

describe('UserServiceTest', () => {
  beforeEach(() => {
    addProviders([UserService]);
  });

  it('#isLoggedIn should return false after creation', inject([UserService], (service: UserService) => {
    expect(service.isLoggedIn()).toBeFalsy();
  }));
});

The first step is to set up the dependencies before every test run with addProviders. Every element in the array will be available for Dependency Injection. After that, the UserService can be used in the inject method to retrieve an instance of the service. In the test, we call the isLoggedIn method to check whether it's falsy and it passes green. It is important that the type definition inside the inject method's callback is only for type checking, not for Dependency Injection.

Introducing the HTTP Service

Let's add a method that can log in the user with an HTTP call.

// src/app/user.service.ts
import { Injectable } from '@angular/core';
import { Http, Headers } from '@angular/http';

@Injectable()
export class UserService {
  ...

  constructor(private http: Http) {}

  ...

  login(credentials) {
    let headers = new Headers();
    headers.append('Content-Type', 'application/json');

    return this.http.post(
      '/login',
      JSON.stringify(credentials),
      { headers }
    );
  }
}

This is the first time that we make use of the Injectable decorator. We import and add the Http service to the dependencies inside the constructor and store it automatically with the private keyword. With the Http service, requests can be made towards the server.

Let's take a closer look at the new login method. It creates a request for the /login endpoint with the given credentials and headers. The body needs to be JSON encoded, because Angular 2 currently only supports text content for the body of the request. If this feature becomes available, we can remove it. Finally, we need to give the Content-Type in the headers to tell the server that we are sending JSON content.

After the code, let's look at the tests. It throws an error saying No provider for Http! (UserService -> Http). The Http service as a dependency is required by the service, the inject method searches for it, but it doesn't find any. It is because we haven't provided it yet. An easy solution might be to add it quickly to the dependencies, but in this case the test would make real HTTP calls. Nobody wants that in a unit test, because the tests wouldn't run in isolation and would depend on external systems.

Instead of the real one, provide a fake backend implementation. We don't need to implement it, Angular 2 already has one.

// src/app/user.service.spec.ts
import { addProviders, inject } from '@angular/core/testing';
import { Http, BaseRequestOptions } from '@angular/http';
import { MockBackend } from '@angular/http/testing';
import { UserService } from './user.service';

...

beforeEach(() => {
  addProviders([
    MockBackend,
    BaseRequestOptions,
    {
      provide: Http,
      useFactory: (backendInstance: MockBackend, defaultOptions: BaseRequestOptions) => {
        return new Http(backendInstance, defaultOptions);
      },
      deps: [MockBackend, BaseRequestOptions]
    },
    UserService
  ]);
});

...

The Http service is provided through a factory method where we can alter it's original setup. Instead of the real backend we give it a MockBackend, which has the same interface as the original one, but doesn't send HTTP requests. This MockBackend will be able to record and answer to calls.

Now, our tests are green again.

The next step is to write a test against the login method. Until now the test we wrote was synchronous, but the test for the login method won't be. For asynchronous tests in Jasmine the done callback given as a parameter must be called after the test is executed. It tells the test runner that the asynchronous test has ended.

// src/app/user.service.spec.ts
it('should send the login request to the server', (done) => {
  done();
});

As you might have noticed, it has a different interface than the inject method. To make them work together, the inject needs to be moved to a beforeEach step. This step will run before every test and instantiation can be moved here.

Here's how the previous test looks like with dependencies refactored:

// src/app/user.service.spec.ts
describe('UserServiceTest', () => {
  let subject: UserService = null;

  beforeEachProviders(() => [ ... ]);

  beforeEach(inject([UserService], (userService: UserService) => {
   subject = userService;
  }));

  it('#isLoggedIn should return false after creation', () => {
    expect(subject.isLoggedIn()).toBeFalsy();
  });
});

Not only does this enable us to write asynchronous tests, but it also eliminates some duplication. Otherwise, we would have had to copy the inject method to every test.

Writing the Test Against an HTTP Call

Finally, we'll write the test against the login method.

// src/app/user.service.spec.ts
import { Http, BaseRequestOptions, Response, ResponseOptions, RequestMethod } from '@angular/http';
import { MockBackend, MockConnection } from '@angular/http/testing';

...

describe('UserServiceTest', () => {
  let subject: UserService = null;
  let backend: MockBackend = null;

  beforeEach(inject([UserService, MockBackend], (userService: UserService, mockBackend: MockBackend) => {
    subject = userService;
    backend = mockBackend;
  }));

  ...

  it('#login should call endpoint and return it\'s result', (done) => {
    backend.connections.subscribe((connection: MockConnection) => {
      let options = new ResponseOptions({
        body: JSON.stringify({ success: true })
      });
      connection.mockRespond(new Response(options));
    });

    subject
      .login({ username: 'admin', password: 'secret' })
      .subscribe((response) => {
        expect(response.json()).toEqual({ success: true });
        done();
      });
  });
});

We needed an instance of MockBackend to listen and answer the requests, so we added it to the inject parameters after UserService. When a request is made, it is emitted on the connections Observable property of the backend. These requests can respond with the mockRespond method if we give it a Response object. In addition to the response's body, we can also provide other properties like headers. At the end of the test, we subscribe to the response of the login method and assert if it has responded correctly.

We can improve it even more by making more assertions on the request. The request's url, method, body and headers can also be checked.

// src/app/user.service.spec.ts

backend.connections.subscribe((connection: MockConnection) => {
  expect(connection.request.method).toEqual(RequestMethod.Post);
  expect(connection.request.url).toEqual('/login');
  expect(connection.request.text()).toEqual(JSON.stringify({ username: 'admin', password: 'secret' }));
  expect(connection.request.headers.get('Content-Type')).toEqual('application/json');

  ...
});

The first assertion is for its method type. Method types are represented as integers and the method-integer associations can be found on RequestMethod as properties. The request's body can be retrieved with the text method. It returns the body as a string, so we need to JSON stringify the credentials object. The headers we set can be accessed via a simple getter method. With these in place, every important aspect of the request can be checked.

Now that we've added the assertions, our first unit test against an Http service is complete.

Conclusion

In this tutorial, we managed to:

  • Create a service that sends a request,
  • Set up required dependencies for our tests with beforeEachProviders and inject,
  • Fake the backend with MockBackend to make testing possible, and
  • Respond and make assertions for the request through MockConnection.

The code for the entire project is available in this GitHub repository.

We hope that this article showed you that writing tests for services isn't a difficult task and encouraged you to write more tests to ensure good business behavior. If you have any comments and questions, feel free to leave them in the comments below.

9d2e715baab928f5bedb837bfcb70b2b
Soós Gábor

A full stack Javascript enthusiast, who loves to fiddle with new things and share it online on Github and Medium. Prefers Extreme Programming and sees his work as craftsmanship.

on this tutorial so far.
User deleted author {{comment.createdAt}}

Edited on {{comment.updatedAt}}

Cancel

Sign In You must be logged in to comment.