Chapter 12. Testing

One of the reasons AngularJS was introduced was a problem of testing front-end apps. It solved that problem quite nicely and version 2 follows that path despite being a totally new piece of code. What makes it a good choice is a separation of concerns. We’ll focus mostly on unit testing here. Its responsibility is to ensure that every single function of a system works fine without being even aware of any other part. If the chosen framework (or library) doesn’t help with such a separation, and doesn’t give ability of mocking dependencies, it is really hard to do it right over time (or even impossible). We won’t focus on the test setup, so please take your time to use angular-cli or any other starter project that suites you to follow. If you’re not familiar with testing that’s OK. We’ll start right now from the very beginning. Later we’ll move to tests connected directly to our app.

First tests

The place to start with is a component. The very basic ‘Hello World’ example looks like this:

import { Component } from '@angular/core';

@Component({
  selector: 'app',
  template: '<span>Hello</span>'
})
export class App {}

It’s basically a class with a decorator, which adds some logic Angular makes use of. The important thing is decorators add some stuff to the class. It means we can still do: new App() and end up with a pure JavaScript object!

Now let’s add some property to the class:

@Component({
  selector: 'app',
  template: '<span>{{ hello }}</span>'
})
export class App {
  public hello: string = 'Hello';
}

And now let’s move to tests. First, you must place the tests in a directory. One way is to keep unit tests as close to the code as possible, and the other is to keep all tests in separate directory. There’s no better solution in fact. Each one has good and bad parts. The choice is yours, but remember, stick to the rules you’ve created. It’s actually more important than the choice.

If we have a file like app.component.ts, which contains the code from the snippet above, then we would create a file named app.component.spec.ts or app.component.test.ts. Put the following in there:

import { App } from './app.component';

describe('App', () => {

  it('should have hello property', () => {
    this.app = new App();
    expect(this.app.hello).not.toBe('Hello');
  });

});

It’s good to write failing tests first and then make it passing – even when no TDD is introduced. It’s just about making sure that a particular test is running and it is not a false positive. In Jasmine you can see a statement like expect(...).not.toBe(...).

We are now able to test Services, Pipes, basic Components and everything what is a JavaScript class.

Let’s move forward and add a method to the component:

@Component({
  selector: 'app',
  template: '<span>{{ sayHello() }}</span>'
})
export class App {
  public name: string = 'John';

  sayHello(): string {
    return `Hello ${this.name}`;
  }
}

And the actual test looks like this:

describe('App', () => {

  beforeEach(() => {
    this.app = new App();
  });

  it('should have name property', () => {
    expect(this.app.name).toBe('John');
  });

  it('should say hello with name property', () => {
    expect(this.app.sayHello()).toBe('Hello John');
  });

});

Services

As far as we know services are nearly pure classes. What makes them different is the ability to inject and be injected. But still it makes them the easiest one to start with. Let’s start with the very beginning:

class TestService {
  public name: string = 'Injected Service';
}

We can go ahead and use something you’ve already learned:

describe('TestService', () => {

  beforeEach(() => {
    this.testService = new TestService();
  });

  it('should have name property set', () => {
    expect(this.testService.name).toBe('Injected Service');
  });

});

Pure service tests can look just like that. But consider following function: bootstrap(App, [TestService]). What it really does is to provide the service to be ready to be injected through the whole App. It means every time we need that service we can simply use the DI. Things get a little bit more complicated when we want to test such a behavior. First of all we won’t use pure Jasmine functions any more. This is what is about to happen:

import {
  async,
  it,
  describe,
  expect,
  inject,
  beforeEach,
  beforeEachProviders
} from '@angular/core/testing';

Note that all of the helpers from Jasmine were replaced by their equivalents from @angular/core/testing. This is because Angular 2 relies heavily on Dependency Injection, and this is not something that Jasmine is aware of. The Angular team made wrappers that add their own logic. The outcome is we can now use inject() instead of an anonymous callback function in it (or beforeEach) that will inject some class.

The test that conforms the bootstrapping with a given provider would look like the following:

describe('TestService Injected', () => {

  beforeEachProviders(() => [TestService]);

  it('should have name property set', inject([TestService], (testService: TestService) => {
    expect(testService.name).toBe('Injected Service');
  }));

});

This way we should end up with a tested injected service.

One more thing will be required now. We have to set providers for tests to be able to run in the real browser:

import { setBaseTestProviders } from '@angular/core/testing';
import {
  TEST_BROWSER_DYNAMIC_APPLICATION_PROVIDERS,
  TEST_BROWSER_DYNAMIC_PLATFORM_PROVIDERS,
} from '@angular/platform-browser-dynamic/testing';

setBaseTestProviders(TEST_BROWSER_DYNAMIC_PLATFORM_PROVIDERS, TEST_BROWSER_DYNAMIC_APPLICATION_PROVIDERS);

Mocking Providers

So far we haven’t solved the problem of Dependency Injection. The last piece is mocking. The whole idea behind the Angular testability is to replace the real provider with a mocked one to ensure that tests are isolated. Mocking it’s really straightforward:

import { provide } from '@angular/core';

class MockTestService {
  public mockName: string = 'Mocked Service';
}

describe('TestService Mocked', () => {

  beforeEachProviders(() => [
    provide(TestService, { useClass: MockTestService })
  ]);

  it('should have name property set', inject([TestService], (testService: TestService) => {
    expect(testService.mockName).toBe('Mocked Service');
  }));

});

In the test case code doesn’t know what exactly the TestService is. It simply doesn’t care – it’s completely transparent. This way we can test the behavior of the component itself, not its dependencies. Moreover, we can use something from both worlds – the real one and the mocked (if really needed) using simple JavaScript inheritance:

class MockTestServiceInherited extends TestService {
  public sayHello(): string {
    return this.name;
  }
}

describe('TestService Mocked Inherited', () => {

  beforeEachProviders(() => [
    provide(TestService, { useClass: MockTestServiceInherited })
  ]);

  it('should say hello with name', inject([TestService], (testService: TestService) => {
    expect(testService.sayHello()).toBe('Injected Service');
  }));

});

Components

Injecting services themselves to test suites is not what you really need. They can be tested using pure classes (unless you want to inject service into service). The point is they gives the possibility to fully test a component. But let’s leave it for now and grab some component.

Let’s say that our component is a list of items (e.g. tasks). It’s a dumb component – all it does is rendering a list for given input. It’s as simple as this:

import { Component, Input } from '@angular/core';
import { NgFor } from '@angular/common';

@Component({
  selector: 'list',
  template: '<span *ngFor="let task of tasks">{{ task }}</span>',
  directives: [NgFor]
})
class ListComponent {
  public tasks: string[] = [];
}

Angular 2 forces us to do a little bit now to create such a component in test:

describe('ListComponent', () => {

  it('should render list', inject([TestComponentBuilder], (tcb: TestComponentBuilder) => {
    return tcb.createAsync(ListComponent).then(componentFixture => {
      const element = componentFixture.nativeElement;
      componentFixture.componentInstance.tasks = ['Learn ng2'];
      componentFixture.detectChanges();
      expect(element.querySelectorAll('span').length).toBe(1);
    });
  }));

});

We don’t want to go in depth when it comes to the provided API, but you should really know these methods of ComponentFixture:

  • nativeElement - allows to access DOM
  • componentInstance - allows to access properties of a component
  • detectChanges - force Angular to run change detection

Moreover inject allows us to have Promise as a test result. This Promise is being created when called createAsync method on TestComponentBuilder. The rest is pretty straightforward. We set array to be some mocked value, say to Angular to detect changes and then check if everything rendered successfully.

As part of successful rendering we should also test events attached inside a particular component. Now consider a component, which is just listening to the click event:

import { Component, Input } from '@angular/core';

@Component({
  selector: 'click-handler',
  template: '<button (click)="sendEvent()"></button>',
})
class ClickHandlerComponent {
  public sendEvent(): void {
      // do nothing
  }
}

To test it we need to introduce the spyOn function that tracks the function invocations. You should do the following:

describe('ClickHandlerComponent', () => {

  it('should invoke a method when clicked', inject([TestComponentBuilder], (tcb: TestComponentBuilder) => {
    return tcb.createAsync(ClickHandlerComponent).then(componentFixture => {
      spyOn(componentFixture.componentInstance, 'sendEvent');
      const element = componentFixture.nativeElement;
      const clickableElement = element.querySelector('button');
      clickableElement.click();
      componentFixture.detectChanges();
      expect(componentFixture.componentInstance.sendEvent).toHaveBeenCalled();
    });
  }));

});

Now we are sure that the event handler was attached successfully and the method has been called properly. Further, we can use more advanced Jasmine matchers to check whether methods were invoked with proper arguments and so on.

Event Emitters and Observables

Many of the components in your app will be just a dumb components. It means they only have to know about the output and the input. Testing @Input() is nothing more just setting proper value. With @Output we do have a similar thing. In unit testing we don’t really want to test internal Angular 2 functions, we just want to make sure our code is working properly. We’ll cover it according to the previous example. Consider the following component:

import { Component, EventEmitter, Input, Output } from '@angular/core';

@Component({
  selector: 'click-handler',
  template: '<button (click)="sendEvent()"></button>',
})
class ClickHandlerComponent {
  @Output() public onButtonClicked: EventEmitter<boolean> = new EventEmitter();

  public sendEvent(): void {
      this.onButtonClicked.emit(true);
  }
}

The point is to test whether onButtonClicked is emitting a value each time sendEvent is invoked. To achieve this we can inject component and subscribe to the event emitter:

describe('ClickHandlerComponent', () => {

  beforeEachProviders(() => [
    ClickHandlerComponent
  ]);

  beforeEach(inject([ClickHandlerComponent], (clickHandlerComponent: ClickHandlerComponent) => {
    this.component = clickHandlerComponent;
  }));

  it('should emit event when a sendEvent method is called', done => {
    this.component.onButtonClicked.subscribe((event: boolean) => {
      expect(event).toBe(true);
      done();
    });
    this.component.sendEvent();
  });

});

Now we’re using the jasmine async version of the it callback to achieve the desired result. Usage of EventEmitter implies such a syntax as it’s no more a Promise to be resolved. So the first thing inside the actual test is to subscribe to the emitter and then invoke a method that should use that emitter. Note that we’re not using @Output() anywhere in the test. We’re just checking the emitter behavior.

Pipes

Every entity in Angular 2 Pipe is also a class with proper annotation. The nice thing about Pipes is they are very small, easy to understand, and easy to test. Let’s say we have to truncate some text and implement a pipe for that:

import { Pipe, PipeTransform } from '@angular/core';

@Pipe({
  name: 'truncate'
})
class TrancatePipe implements PipeTransform {
  public transform(value: string): string {
    return value.length > 10 ? `${value.substring(0, 10)}...` : value;
  }
}

All the purpose of the above pipe is to cut everything after the 10th letter and add there an ellipsis. To make a proper test we can use everything we’ve learned so far. Pipe is just a specific class. So all we need is to test the transform method:

describe('TrancatePipe', () => {

  beforeEachProviders(() => [
    TrancatePipe
  ]);

  beforeEach(inject([TrancatePipe], (trancatePipe: TrancatePipe) => {
    this.pipe = trancatePipe;
  }));

  it('should truncate long text', () => {
    expect(this.pipe.transform('very long text')).toBe('very long ...');
  });

  it('should NOT truncate short text', () => {
    expect(this.pipe.transform('short one')).toBe('short one');
  });

});

The fact that a given pipe is stateless and allowed as to move injection of the pipe to the beforeEach, but be very careful with that. Ideally, unit tests should be as separated as possible, but it sometimes conflicts with DRY (Don’t Repeat Yourself) so we strongly advise you have balance there. Sometimes it’s worth it to copy some init in a test due to the readability and reasoning about the code.

Router

Everything above is fine up to the point when you meet Router. This one requires to add some additional steps to make tests work. Let’s start with the following snippet:

import { Component } from '@angular/core';
import { ROUTER_DIRECTIVES, Routes } from '@angular/router';

import { ChildComponent } from './components/child.component';

@Component({
  selector: 'app',
  template: ``,
  directives: ROUTER_DIRECTIVES
})
@Routes([
  { path: '/', component: ChildComponent }
])
class AppComponent {}

Note, we just added only the directives (ROUTER_DIRECTIVES) with no template. Test for that component would start with this:

describe('AppComponent', () => {

  it('should be able to test', inject([TestComponentBuilder], (tcb: TestComponentBuilder) => {
    return tcb.createAsync(AppComponent).then(componentFixture => {
      componentFixture.detectChanges();
      expect(true).toBe(true);
    });
  }));

});

It’s still fine. But the problem is that @Routes just sets properties and doesn’t make our component work as expected. We have to add a place where the component for the given path will be rendered. There’s a directive for that called RouterOutlet. Here it goes:

import { Component } from '@angular/core';
import { ROUTER_DIRECTIVES, Routes } from '@angular/router';

import { ChildComponent } from './components/child.component';

@Component({
  selector: 'app',
  template: `<router-outlet></router-outlet>`,
  directives: ROUTER_DIRECTIVES
})
@Routes([
  { path: '/', component: ChildComponent }
])
class AppComponent {}

Now our test fails with:

ORIGINAL EXCEPTION: No provider for Router!

To fix that we have to provide a fake router. To achieve this we are able to import ROUTER_FAKE_PROVIDERS and simply add it into the beforeEach section:

describe('AppComponent', () => {

  beforeEachProviders(() => [ROUTER_FAKE_PROVIDERS])

  it('should be able to test', inject([TestComponentBuilder], (tcb: TestComponentBuilder) => {
    return tcb.createAsync(AppComponent).then(componentFixture => {
      componentFixture.detectChanges();
      expect(true).toBe(true);
    });
  }));

});

As you can see, it is pretty straightforward to satisfy the app about the router. The problem really begins when you want to test the routing itself. It very quickly ends up with imports like this:

import { ComponentResolver } from '@angular/core';
import { Location } from '@angular/common';
import {
  DefaultRouterUrlSerializer,
  RouteSegment,
  Router,
  RouterOutletMap,
  RouterUrlSerializer
} from '@angular/router';
import {
  beforeEach,
  beforeEachProviders,
  describe,
  expect,
  fakeAsync,
  inject,
  it,
  tick
} from '@angular/core/testing';
import { SpyLocation } from '@angular/common/testing';
import { TestComponentBuilder } from '@angular/compiler/testing';

What happened here? Now we do have to make a lot of mocking that we are used to. By a lot, we mean:

beforeEachProviders(() => [
   RouterOutletMap,
   { provide: Location, useClass: SpyLocation },
   { provide: RouterUrlSerializer, useClass: DefaultRouterUrlSerializer },
   {
     provide: Router,
     deps: [ComponentResolver, RouterUrlSerializer, RouterOutletMap, Location],
     useFactory: (resolver, urlParser, outletMap, location) =>
       new Router('AppComponent', AppComponent, resolver, urlParser, outletMap, location)
   }
 ]);

We are now about to test the following routing applied to the component:

@Routes([
  { path: '/test', component: TestComponent }
])

The test itself is pretty nice-looking:

it('should be able to navigate to TestComponent',
    fakeAsync(inject([Router, Location, TestComponentBuilder],
      (router: Router, location: Location, tcb: TestComponentBuilder) => {
        tcb.createFakeAsync(AppComponent);
        router.navigate(['/test']);
        tick();
        expect(location.path()).toBe('/test');
      })
  ));

This time we used fakeAsync. It let’s us create asynchronous tests in a synchronous way. The key here is a tick function. It says to the Angular to finish the jobs that just left. In this case it’s a navigation.

HTTP

The common pattern is to keep connection with back-end outside the component and simply not to inject http directly into the component. The right place seems to be a service. So let’s create one:

import { Injectable } from '@angular/core';
import { Http } from '@angular/http';

@Injectable()
export class TestService {
  constructor(private http: Http) {}

  getUsers() {
    return this.http.get('http://foo.bar');
  }
}

You know pretty much everything that happened right now, so let’s move to the test. These imports will be useful:

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

Now, let’s use it and prepare proper services to be injected:

beforeEachProviders(() => [
  TestService,
  BaseRequestOptions,
  MockBackend,
  provide(Http, {
    useFactory: (backend: MockBackend, defaultOptions: BaseRequestOptions) => {
      return new Http(backend, defaultOptions);
    },
    deps: [MockBackend, BaseRequestOptions]
  })
]);

One thing to notice is the Http mock. Each time when something needs the Http we are using our concrete implementation. Moreover, we are injecting dependencies into the factory. It’s worth it to add that in case factory returns an instance of class it won’t be a singleton.

With mocked Http there’s one thing left before the actual test. I’ve mocked all HTTP calls to return a simple string:

beforeEach(inject([MockBackend], (backend: MockBackend) => {
  const baseResponse = new Response(new ResponseOptions({ body: 'got response' }));
  backend.connections.subscribe((c: MockConnection) => c.mockRespond(baseResponse));
}));

And finally the test itself:

it('should return response when subscribed to getUsers',
  inject([TestService], (testService: TestService) => {
    testService.getUsers().subscribe((res: Response) => {
      expect(res.text()).toBe('got response');
    });
  })
);

We just tested that every time we call getUsers it makes an HTTP call.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset