Angular 2 Final Release Router Unit Test

27,398

For testing we now create a testing module using TestBed. We can use the TestBed#configureTestingModule and pass a metadata object to it the same way we would pass to @NgModule

beforeEach(() => {
  TestBed.configureTestingModule({
    imports: [ /* modules to import */ ],
    providers: [ /* add providers */ ],
    declarations: [ /* components, directives, and pipes */ ]
  });
});

For routing, instead of using the normal RouterModule, we would instead use RouterTestingModule. This sets up the Router and Location, so you don't need to yourself. You can also pass routes to it, by calling RouterTestingModule.withRoutes(Routes)

TestBed.configureTestingModule({
  imports: [
    RouterTestingModule.withRoutes([
      { path: 'home', component: DummyComponent }
    ])
  ]
})

To get the Location and Router in the test, the same thing works, as in your example.

let router, location;

beforeEach(() => {
  TestBed...
});

beforeEach(inject([Router, Location], (_router: Router, _location: Location) => {
  router = _router;
  location = _location;
}));

You could also inject into each test as necessary

it('should go home',
    async(inject([Router, Location], (router: Router, location: Location) => {
})));

The async above is used like done except we don't need to explicitly call done. Angular will actually do that for us after all asynchronous tasks are complete.

Another way to get the providers is from the test bed.

let location, router;

beforeEach(() => {
  TestBed.configureTestingModule({
    imports: [RouterTestingModule.withRoutes([
      { path: 'home', component: DummyComponent }
    ])],
  });
  let injector = getTestBed();
  location = injector.get(Location);
  router = injector.get(Router);
});

Here's a complete test, refactoring your example

import { Component } from '@angular/core';
import { Location } from '@angular/common';
import { Router } from '@angular/router';
import { RouterTestingModule } from '@angular/router/testing';
import { fakeAsync, async, inject, TestBed, getTestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';

@Component({
  template: `
    <router-outlet></router-outlet>
  `
})
class RoutingComponent { }

@Component({
  template: ''
})
class DummyComponent { }

describe('component: RoutingComponent', () => {
  let location, router;

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [RouterTestingModule.withRoutes([
        { path: 'home', component: DummyComponent }
      ])],
      declarations: [RoutingComponent, DummyComponent]
    });
  });

  beforeEach(inject([Router, Location], (_router: Router, _location: Location) => {
    location = _location;
    router = _router;
  }));

  it('should go home', async(() => {
    let fixture = TestBed.createComponent(RoutingComponent);
    fixture.detectChanges();
    router.navigate(['/home']).then(() => {
      expect(location.path()).toBe('/home');
      console.log('after expect');
    });
  }));
});

UPDATE

Also, if you want to simply mock the router, which actually might be the better way to go in a unit test, you could simply do

let routerStub;

beforeEach(() => {
  routerStub = {
    navigate: jasmine.createSpy('navigate'),
  };
  TestBed.configureTestingModule({
    providers: [ { provide: Router, useValue: routerStub } ],
  });
});

And in your tests, all you want to do is test that the stub is called with the correct argument, when the component interacts with it

expect(routerStub.navigate).toHaveBeenCalledWith(['/route']);

Unless you actually want to test some routing, this is probably the preferred way to go. No need to set up any routing. In a unit test, if you are using real routing, you're involving unnecessary side effects that could affect what you are really trying to test, which is just the behavior of the component. And the behavior of the component is to simply call the navigate method. It doesn't need to test that the router works. Angular already guarantees that.

Share:
27,398
xphong
Author by

xphong

phong.io

Updated on September 19, 2020

Comments

  • xphong
    xphong over 3 years

    How do I unit test routers in Angular version 2.0.0 with karma and jasmine?

    Here's what my old unit test looks like in version 2.0.0-beta.14

    import {
      it,
      inject,
      injectAsync,
      beforeEach,
      beforeEachProviders,
      TestComponentBuilder
    } from 'angular2/testing';
    
    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';
    import { provide } from 'angular2/core';
    
    import { App } from './app';
    
    describe('Router', () => {
    
      let location, router;
    
      beforeEachProviders(() => [
        RouteRegistry,
        provide(Location, {useClass: SpyLocation}),
        provide(Router, {useClass: RootRouter}),
        provide(ROUTER_PRIMARY_COMPONENT, {useValue: App})
      ]);
    
      beforeEach(inject([Router, Location], (_router, _location) => {
        router = _router;
        location = _location;
      }));
    
      it('Should be able to navigate to Home', done => {
        router.navigate(['Home']).then(() => {
          expect(location.path()).toBe('');
          done();
        }).catch(e => done.fail(e));
      });
    
    });

  • xphong
    xphong over 7 years
    Thanks for the descriptive answer. Can you explain what the DummyComponent and RoutingComponent are used for?
  • Paul Samsotha
    Paul Samsotha over 7 years
    From what I was able to test, the router wouldn't work by itself without some configured routes. So I configured a root class and a single route that goes to the dummy component. The dummy comonent's path is home
  • Shane Voisard
    Shane Voisard over 7 years
    The docs incorrectly use modules instead of imports in the configuration object passed to TestBed.configureTestingModule.
  • Galdor
    Galdor over 7 years
    syntax errors corrected: { provide: Router, useValue: routerStub}, expect (routerStub.navigate).toHaveBeenCalledWith (['/route']);
  • František Žiačik
    František Žiačik about 7 years
    Please note, with angular2 version 2.4.9, you need to have routerState in the mock: routerStub = { navigate: ..., routerState: {} }, otherwise you'll get an error. Also, when mocking with useFactory, one must write it like this: useFactory: function() { ... }, not useFactory: () => { ... }, otherwise another error occurs.
  • Intellix
    Intellix about 7 years
    Just want to add that the navigate().then promise is important as otherwise the test runs too fast and other tests start throwing.
  • Manu Chadha
    Manu Chadha over 5 years
    Paul - will your answer work on Angular6? I tried the approach but location.path() is not defined in my set up. Could you please take a look a stackoverflow.com/questions/53823091/…
  • Manu Chadha
    Manu Chadha over 5 years
    don't bother Paul. Just solved it. It seems a different definition of Location was being picked. The code worked when I added import {Location} from "@angular/common";
  • ismaestro
    ismaestro about 5 years
    When I do this, it says: TypeError: Cannot read property 'root' of undefined
  • ismaestro
    ismaestro about 5 years
  • Bozhinovski
    Bozhinovski over 2 years
    @ismaestro did manage to resolve that issue with TypeError: Cannot read property 'root' of undefined ?