Start building
Updates

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

Wojciech Kwiatek, Feb 9, 2016


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.


Latest articles

Article banner: What Do Software Engineers Do? Roles, Responsibilities, and Skills Explained

Dec 13, 2024

What Do Software Engineers Do? Roles, Responsibilities, and ...

Article banner: How to Get Started in Cyber Security: A Beginner’s Guide

Dec 6, 2024

How to Get Started in Cyber Security: A Beginner’s Guide

Article banner: What is Binary Code? Modern Language to the Binary System

Nov 20, 2024

What is Binary Code? Modern Language to the Binary System