Tuesday, May 9, 2017

Testing your Services with Angular

Have you ever joined a project to find out it is missing unit tests?
So as the enthusiastic new comer, you've decided to roll your sleeves 💪 and you're up to add more unit tests. In this article, I'd like to share the fun of seeing the code coverage percentage increased 📊 📈 in your angular application.

I love angular.io documentation. Really great content and since it is part of angular repo it's well maintained an kept up-to-date. To start with I recommend you reading Testing Advanced cookbook.

When starting testing a #UnitTestLackingApplication, I think tackling Angular Services are easier to start with. Why? Some services might be self contained object without dependencies, or (more frequently the only dependencies might be with http module) and for sure, there is no DOM testing needed as opposed to component testing.

Setting up tests


I'll use the code base of openshift.io to illustrate this post. It's a big enough project to go beyond the getting started apps. Code source could be found in: https://github.com/fabric8io/fabric8-ui/

From my previous post "Debug your Karma", you know how to run unit test and collect code coverage from Istanbul set-up. Simply run:
npm test // to run all test
npm run test:unit // to run only unit test (the one we'll focus on)
npm run test:debug // to debug unit test 

When starting a test you'll need:
  • to have an entry point, similar to having a main.ts which will call TestBed.initTestEnvironment, this is done once for all your test suite. See spec-bundle.js for a app generated using AngularClass starter.
  • you also need a "root" module similar (called testing module) to a root module for your application. You'll do it by using TestBed.configureTestingModule. This is something to do for each test suite dependent on what you want to test.
Let's delve into more details and talk about dependency injection:

Dependency Injection


The DI in Angular consists of:
  • Injector - The injector object that exposes APIs to us to create instances of dependencies. In your case we'll use TestBest which inherits from Injector.
  • Provider - A provider takes a token and maps that to a factory function that creates an object.
  • Dependency - A dependency is the type of which an object should be created.
Let's add unit test for Codebases service. The service Add/Retrieve list of code source. First we need to know all the dependencies the service uses so that for each dependencies we define a provider. Looking at the constructor we got the information:
@Injectable()
export class CodebasesService {
  ...
  constructor(
      private http: Http,
      private logger: Logger,
      private auth: AuthenticationService,
      private userService: UserService,
      @Inject(WIT_API_URL) apiUrl: string) {
      ...
      }

The service depends on 4 services and one configuration string which is injected. As we want to test in isolation the service we're going to mock most of them.

How to inject mock TestBed.configureTestingModule


I choose to use Logger (no mock) as the service is really simple, I mock Http service (more on that later) and I mock AuthenticationService and UserService using Jasmine spy. Eventually I also inject the service under test CodebasesService.
beforeEach(() => {
      mockAuthService = jasmine.createSpyObj('AuthenticationService', ['getToken']);
      mockUserService = jasmine.createSpy('UserService');

      TestBed.configureTestingModule({
        providers: [
        Logger,
        BaseRequestOptions,
        MockBackend,
          {
            provide: Http,
            useFactory: (backend: MockBackend,
              options: BaseRequestOptions) => new Http(backend, options),
            deps: [MockBackend, BaseRequestOptions]
          },
          {
            provide: AuthenticationService,
            useValue: mockAuthService
          },
          {
            provide: UserService,
            useValue: mockUserService
          },
          {
            provide: WIT_API_URL,
            useValue: "http://example.com"
          },
          CodebasesService
        ]
      });
    });

One thing important to know is that with DI you are not in control of the singleton object created by the framework. This is the Hollywood concept: don't call me, I'll call you. That's why to get the singleton instance created for CodebasesService and MockBackend, you need to get it from the injector either using inject as below:
beforeEach(inject(
  [CodebasesService, MockBackend],
  (service: CodebasesService, mock: MockBackend) => {
    codebasesService = service;
    mockService = mock;
  }));

or using TestBed.get:

 beforeEach(() => {
   codebasesService = TestBed.get(CodebasesService);
   mockService = TestBed.get(MockBackend);
});

To be or not to be


Notice how you get the instance created for you from the injector TestBed. What about the mock instance you provided with useValue? Is it the same object instance that is being used? Interesting enough if your write a test like:
it('To be or not to be', () => {
   let mockAuthServiceFromDI = TestBed.get(AuthenticationService);
   expect(mockAuthService).toBe(mockAuthServiceFromDI); // [1]
   expect(mockAuthService).toEqual(mockAuthServiceFromDI); // [2]
 });

line 1 will fail whereas line 2 will succeed. Jasmine uses toBe to compare object instance whereas toEqual to compare object's values. As noted in Angular documentation, the instances created by the injector are not the ones you used for the provider factory method. Always, get your instance from the injector ie: TestBed.

Mocking Http module to write your test


Using HttpModule in TestBed


Let's revisit our TestBed's configuration to use HttpModule:
beforeEach(() => {
      mockLog = jasmine.createSpyObj('Logger', ['error']);
      mockAuthService = jasmine.createSpyObj('AuthenticationService', ['getToken']);
      mockUserService = jasmine.createSpy('UserService');

      TestBed.configureTestingModule({
        imports: [HttpModule], // line [1]
        providers: [
          Logger,
          {
            provide: XHRBackend, useClass: MockBackend // line [2]
          },
          {
            provide: AuthenticationService,
            useValue: mockAuthService
          },
          {
            provide: UserService,
            useValue: mockUserService
          },
          {
            provide: WIT_API_URL,
            useValue: "http://example.com"
          },
          CodebasesService
        ]
      });
      codebasesService = TestBed.get(CodebasesService);
      mockService = TestBed.get(XHRBackend);
    });

By adding an HttpModule to our testing module in line [1], the providers for Http, RequestOptions is already configured. However, using an NgModule’s providers property, you can still override providers (line 2) even though it has being introduced by other imported NgModules. With this second approach we can simply override XHRBackend.

Mock http response


Using Jasmine DBB style, let's test the addCodebase method:
it('Add codebase', () => {
      // given
      const expectedResponse = {"data": githubData};
      mockService.connections.subscribe((connection: any) => {
        connection.mockRespond(new Response(
          new ResponseOptions({
            body: JSON.stringify(expectedResponse),
            status: 200
          })
        ));
      });
      // when
      codebasesService.addCodebase("mySpace", codebase).subscribe((data: any) => {
        // then
        expect(data.id).toEqual(expectedResponse.data.id);
        expect(data.attributes.type).toEqual(expectedResponse.data.attributes.type);
        expect(data.attributes.url).toEqual(expectedResponse.data.attributes.url);
      });
  });

Let's do our testing using the well-know given, when, then paradigm.

We start with given: Angular’s http module comes with a testing class MockBackend. No http request is sent and you have an API to mock your call. Using connection.mockResponse we can mock the response of any http call. We can also mock failure (a must-have to get a 100% code coverage 😉) with connection.mockError.

The when is simply about calling our addCodebase method.

The then is about verifying the expected versus the actual result. Because http call return RxJS Observable, very often service's method that use async REST call will use Observable too. Here our addCodebase method return a Observable. To be able to unwrap the Observable use the subscribe method. Inside it you can access the Codebase object and compare its result.


What's next?


In this post you saw how to test a angular service using http module. You can get the full source code in github.
You've seen how to set-up a test with Dependency Injection, how to mock http layer and how to write your jasmine test. Next blog post, we'll focus on UI layer and how to test angular component.

No comments:

Post a Comment

Note: Only a member of this blog may post a comment.