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();
});
});
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();
});
});
Using anonymous ES 6 class and jasmine:
beforeEach(() => addProviders([
{
provide: Router,
useClass: class { navigate = jasmine.createSpy("navigate"); }
}]));
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();
});
});
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)
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));
});
});
});
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();
}));