Testing Angular 2 Apps (Part 3): RouterOutlet and API

If you missed Part 1 and Part 2 of our Testing Angular 2 Apps series, I encourage you to take a look at them.

Note: Article is based on Angular 2 beta.3

Up-to-date Gist with examples for whole series is here: Angular 2 testing gist

Making tests work with Router

The first real problem you’ll meet when trying to test your real app will be the Router. What’s wrong with it? Let’s try to simply add a RouteConfig to the App component:

import {Component} from 'angular2/core';
import {RouteConfig} from 'angular2/router';

import {TestComponent} from './components/test';

@Component({
  selector: 'app',
  template: ''
})
@RouteConfig([
  { path: '/', component: TestComponent }
])
export class App {}

Our simplest test would look like this:

describe('App', () => {

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

});

Note: do you remember there should be added setBaseTestProviders to make tests work in a browser?

It’s all fine! But the problem is that RouteConfig just sets properties and doesn’t make our component work as expected. We have to add place where component for given path will be rendered. There’s directive for that called RouterOutlet. And now the party begins. Here it goes:

import {Component} from 'angular2/core';
import {RouteConfig, RouterOutlet} from 'angular2/router';

import {TestComponent} from './components/test';

@Component({
  selector: 'app',
  template: '<router-outlet></router-outlet>',
  directives: [RouterOutlet]
})
@RouteConfig([
  { path: '/', component: TestComponent }
])
export class App {}

And what happens now? Yep, it fails:

Failed: No provider for Router! (RouterOutlet -> Router)

So let’s add a Router:

beforeEachProviders(() => [
  provide(Router)
]);

You’ll end up with such a happy error:

Failed: Cannot resolve all parameters for 'Router'(?, ?, ?).

The way to solve it is to use RootRouter when app asks about Router:

beforeEachProviders(() => [
  provide(Router, {useClass: RootRouter})
]);

But now it still fails:

Failed: No provider for RouteRegistry! (RouterOutlet -> Router -> RouteRegistry)

This is because RootRouter requires 3 parameters to be injected:

  • registry
  • location
  • primaryComponent

In case of tests we can provide them using SpyLocation mock for Location provider and App as ROUTER_PRIMARY_COMPONENT. Providers now should look like these:

beforeEachProviders(() => [
  RouteRegistry,
  provide(Location, {useClass: SpyLocation}),
  provide(ROUTER_PRIMARY_COMPONENT, {useValue: App}),
  provide(Router, {useClass: RootRouter})
]);

Bingo! Now we do have working Router injection. It should fix tests when you’re using router in your application.

Note that we’re not talking about testing routes, url changes etc. but just adding required dependencies for routerOutlet.

Http in services

But let’s move forward to another topic. Do you remember how to test services? Now we need them again.

Common pattern is to keep connection with backend 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 'angular2/core';
import {Http} from 'angular2/http';

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

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

If you watch closely you’ve probably just realised I added something what I was not talking about before, right? It’s Injectable decorator. Why do we need this now? Before I said that services are pure JS classes without any special decorator needed. That’s true unless you’d like to inject something into a service. Why is that? Because Angular 2 makes dependency injections by looking for class’ metadata. These metadata are being added when you add some decorator like @Component or @Directive but in case of services we don’t need anything like this. So to tell Angular we want to inject something we can add just @Injectable.

Note: It seems to be a good practice to add @Injectable for every service (even when you don’t need to inject anything) but it’s crucial to understand why it’s here.

I also injected Http. Are you familiar with $http from AngularJS? That one was using Promises and it worked fine but the new one is built on the top of RxJS. And it’s extremely powerful tool.

A encourage you to take a look at the rx-book. Keep in mind Angular uses v5.0 which is in beta now.

Let’s test this service. Some imports will be handy:

import {BaseRequestOptions, Response, ResponseOptions, Http} from 'angular2/http';
import {MockBackend, MockConnection} from 'angular2/http/testing';

Now we can inject it into test cases:

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 to add that in case factory returns 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 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 there you go! Finally we can create our test case:

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

Now it’s your turn to play, especially with MockConnection and RxJS operators. It should give you the kickstart to the Angular 2 testing and understand what’s really going on there. Now you can take a look at Angular core tests and look there for solutions to specific problems.

If there’s anything I missed or you’d like to discuss some solution please leave your thoughs in comments.

Tagged: , , , , , , ,

Categorised in: ,

  • Riley Warner

    Thanks for creating these articles. The content is very helpful, but the writing is a little hard to follow. Do you have an editor? If not, I would be glad to help.

  • Harald Rietman

    Nice article, good explanation. Do you know how you could mock an HTTP backend when using Protractor for integration tests? Basically what I need is some dumb mocked json responses when the app is tested in the browser.

    • Maruk

      try json-server and change endpoints in your test config

      • Harald Rietman

        Thanks, will have a look at it.

  • Thanks again I really got a lot from these testing blogs :)

  • JDillon

    Where do we import half these things from? `RootRouter` is nowhere to be found on the angular.io api documentation. If you provided a full source than I could figure it out that way.

    • Wojciech Kwiatek

      Your IDE should help you with this but in case it didn’t:

      import { RootRouter } from 'angular2/src/router/router';

      import { Location, RouteParams, Router, RouteRegistry, ROUTER_PRIMARY_COMPONENT } from 'angular2/router';

      import { SpyLocation } from 'angular2/src/mock/location_mock';

      • Brian Xu

        I found `Location` is now in ‘angular2/platform/common’; for beta.16

        • Wojciech Kwiatek

          True. Some things changed by the way. I’ll try to update snippets as soon as possible.

          • 163satish

            @wojciech_kwiatek:disqus , when will this tutorial be updated considering the rc4 changes in Angular2 on 30th June, 2016?

          • Wojciech Kwiatek

            Blog posts themselves won’t be touched (sorry) but you can still take a look at the gist referred at the beginning. It’s my source to test changes in field of testing Angular 2 apps so I’ll try to make it up to date. I’m talking about this link: https://gist.github.com/wkwiatek/e8a4a9d92abc4739f04f5abddd3de8a7

  • Stephan Beal

    FYI, the “Angular core tests” link is now 404.

  • Jordan Hansen

    I’m having an issue doing http requests with the expect inside the callback. It looks like yours is working as you would expect, however. There was no problem with the callback for you?

    • Jordan Hansen

      Got it. Just need to pull in `done` and then call it in the callback.

      it(‘can connect to logout route and get JSON response’, done => {

      service.logout().subscribe(res => {

      expect(res.json()).toBeDefined();

      done();

      });

      });

  • 163satish

    When will this tutorial be updated considering the recent changes in rc4?

  • All this is very interesting with the program code! :)

  • charlesg

    Very interesting :),
    however does s.o knows how to provide http not mocked ????

    Many thanks!!!!