Angular2 RC.1 - Inject Router into unit test

2019-04-30 09:50发布

问题:

I'm writing my ng2 tests, and I'm running into some trouble injecting Router into my component for my test. The constructor for my component only takes one argument-- private router: Router.

But, when I run my test case, I'm getting an error while it's trying to inject the Router. What am I doing wrong? Can anyone provide a working example?

I'm using angular2-RC.1

Here's the error I'm getting: No provider for ComponentResolver! (Router -> ComponentResolver)

Here's my test:

import {describe, it, expect, beforeEach, afterEach, beforeEachProviders, inject} from "@angular/core/testing";
import {ReflectiveInjector, provide} from "@angular/core";
import {HTTP_PROVIDERS} from "@angular/http";
import {Router, ROUTER_PROVIDERS} from "@angular/router";
import {ROUTER_FAKE_PROVIDERS} from "@angular/router/testing";
import {Location} from "@angular/common";
import {SpyLocation} from "@angular/common/testing/location_mock";
import {Observable} from "rxjs/Observable";
import {MyComp} from "./MyComp";

describe("MyComp", () => {

  let injector: ReflectiveInjector,
      myComp: MyComp,
      router: Router;

  beforeEach(() => {

    injector = ReflectiveInjector.resolveAndCreate([         
      HTTP_PROVIDERS,
      ROUTER_FAKE_PROVIDERS,          
      provide(Location, {useClass: SpyLocation})

    ]);

    router = injector.get(Router);
    myComp = new MyComp(router);


  });    

  afterEach(() => {
    injector = null;    
    myComp = null;
    router = null; 
  });

  it("should be defined", () => {

    expect(myComp).toBeDefined();

  });



});

回答1:

You need to create a jasmine spy object for the router. AppComponent has a constructor that takes in a Router.

import { AppComponent } from './app.component';
import {Router, ROUTER_PROVIDERS} from "@angular/router";

describe('app component', () => {
    let component: AppComponent;
    let router: Router;
    beforeAll(() => {
        router = jasmine.createSpyObj("Router", ['navigate']);
        component = new AppComponent(router);
    });
    it("should be defined", () => {
        expect(component).toBeDefined();
    });
});


回答2:

Using anonymous ES 6 class and jasmine:

beforeEach(() => addProviders([
{ 
    provide: Router, 
    useClass: class { navigate = jasmine.createSpy("navigate"); }
}]));


回答3:

Some modifications... this simple solution works for me:

import {MyComp} from "./MyComp";
import {RootRouter} from 'angular2/src/router/router';
import {provide} from 'angular2/core'; 
import {Router} from 'angular2/router';

describe("MyComp", () => {
  let myComp: MyComp,
  router;

beforeEach(() => {
  provide(Router, {useClass: RootRouter})
  myComp = new MyComp(router);
}) 

it("should be defined", () => {
  expect(myComp).toBeDefined();
  });
});


回答4:

Well, I came up with a solution. It's not ideal, but it works. Basically, I'm creating a MockRouter class that implements the methods I need.

MockRouter:

export class MockRouter {

    public navigate() {
        console.log(“Mock router was called.”);
    }

}

Now, in my test case, all I have to do is provide the mock implementation for router:

provide(Router, {useClass: MockRouter})

It would be really nice if the NG2 docs would show how you can properly inject Router into your Jasmine test cases. Mocking their objects seems like it should be an unnecessary step.

(FYI, I tried using the ROUTER_FAKE_PROVIDERS and still got the ComponentResolver error above)



回答5:

Here is an alternative solution which is a bit more verbose but allows us to use SpyLocation in order to check the route changes. First we create generic test router providers.

router-test-providers.ts

import { ComponentResolver } from '@angular/core';
import { Type } from '@angular/core/src/facade/lang';
import { SpyLocation } from '@angular/common/testing';
import { Location } from '@angular/common';
import { Router, RouterOutletMap } from '@angular/router';
import { RouteSegment } from '@angular/router/src/segments';
import { RouterUrlSerializer, DefaultRouterUrlSerializer } from '@angular/router/src/router_url_serializer';
/**
 * this class provides the means of loading the tested component type
 */
export class FakeRootComponentLoader {
    constructor(private rootComponentType: Type) {
      this.rootComponentType = rootComponentType;
    }
    public getRootComponentType = () => {
        return this.rootComponentType;
    }
 }

let routerFactory = function (
        fakeRootComponentLoader: FakeRootComponentLoader,
        componentResolver: ComponentResolver,
        urlSerializer: RouterUrlSerializer,
        routerOutletMap: RouterOutletMap,
        location: Location): Router
{
  let fakeRootComponentType = fakeRootComponentLoader.getRootComponentType();
  /**
   * _rootComponent should not be null, but it is what in angular2 rc.1 code
   * so we replicate the behaviour
   */
  return new Router(
      null,
      fakeRootComponentType,
      componentResolver,
      urlSerializer,
      routerOutletMap,
      location);
};

export const ROUTER_TEST_PROVIDERS: any[] = [
  {provide: RouterUrlSerializer, useClass: DefaultRouterUrlSerializer},
  RouterOutletMap,
  {provide: Location, useClass: SpyLocation},
  {provide: RouteSegment, useFactory: (r) => r.routeTree.root, deps: [Router]},
  {
    provide: Router,
    useFactory: routerFactory,
    deps: [FakeRootComponentLoader, ComponentResolver, RouterUrlSerializer, RouterOutletMap, Location]
  }
];

The corresponding jasmine test is provided below.

navigation.spec.ts

import { Component } from '@angular/core';
import { beforeEach, beforeEachProviders, inject } from '@angular/core/testing';
import { ROUTER_DIRECTIVES, Route, Routes, Router } from '@angular/router';
import { TestComponentBuilder } from '@angular/compiler/testing';
import { Location } from '@angular/common';
import { ROUTER_TEST_PROVIDERS, FakeRootComponentLoader } from './router-test-providers';
/**
 * We inject router into the EmptyComponent,
 * Due to the way DI works in angular2, if we import the ROUTER_TEST_PROVIDERS, 
 * and inject the Router, we will get our own implementation of the Router injected.
 */
@Component({selector: 'empty-component', template: `empty`})
@Component({
  selector: 'empty-component',
  template: `empty`,
  directives: [ROUTER_DIRECTIVES]
})
class EmptyComponent {
    constructor (private router: Router){ }
    public getRouter() {return this.router;}
}

@Component({
  selector: 'root-component',
  template: `<router-outlet></router-outlet>`,
  directives: [ROUTER_DIRECTIVES]
})
@Routes([new Route({path: '/login', component: EmptyComponent})])
class RootComponent { }
describe('navigation', () => {
    beforeEachProviders(() => [
        {
            provide: FakeRootComponentLoader,
            useFactory: () => new FakeRootComponentLoader(RootComponent)
        },
        ROUTER_TEST_PROVIDERS,
        EmptyComponent
    ]);


    let location: Location;
    let testCb: TestComponentBuilder;
    let emptyComp: EmptyComponent;
    beforeEach(inject([Location, TestComponentBuilder, EmptyComponent], (loc, tcb, emptyCt) => {
        location = loc;
        testCb = tcb;
        emptyComp = emptyCt;

    }));

    it('should be defined', () => {
        expect(EmptyComponent).toBeDefined();
    });

    it('Should navigate to login', (done) => {
        expect(location.path()).toEqual('');
        testCb.createAsync(RootComponent).then(fixture => {
            emptyComp.getRouter().navigate(['login']).then(() => {
                fixture.detectChanges();
                expect(location.path()).toBe('/login');
                done();
            }).catch(e => done.fail(e));
        });
    });
});


回答6:

After countless of suggestions that didn't work: This was one solution that worked for me in Angular4 and Karma:

It's based on Andriy Tolstoy's Answer and reading through the Angular Manual.

//ToBeTestedComponent.spec.ts

beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [ ToBeTestedComponent],
      providers: [
        {
          provide: Router,
          useClass: class { navigate = jasmine.createSpy("navigate"); }
        }
      ]
    })
    .compileComponents();
  }));