I am writing a spec for an Angular component that displays a button that will navigate to another page. The component makes use of Router::navigate() but does not itself have a router outlet. A parent component has the outlet. In my spec, the test should confirm that clicking on the button routes to the correct path.
My current (broken) spec tries to use RouterTestingModule to provide a route to a DummyComponent. When the button is clicked in the spec I get the following error:
'Unhandled Promise rejection:', 'Cannot find primary outlet to load 'DummyComponent'', '; Zone:', 'angular', '; Task:', 'Promise.then', '; Value:', Error{__zone_symbol__error: Error{originalStack: 'Error: Cannot find primary outlet to load 'DummyComponent'
Obviously I am approaching this problem in the wrong manner. What is the correct way to test router navigation when the component does not have a router outlet?
The component (pseudo-code):
#Component({
template: `
Go to the <button (click)="nextPage">next page</button>
`
})
export class ExampleComponent {
public myId = 5;
constructor(private _router: Router);
public nextPage(): void {
this._router.navigate(['/example', this.myId]);
}
}
The spec. This does not work:
const FAKE_ID = 999;
describe('ExampleComponent Test', () => {
let exampleComponent: ExampleComponent;
let fixture: ComponentFixture<ExampleComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [ DummyComponent ],
imports: [
RouterTestingModule.withRoutes([
{ path: 'example/:id', component: DummyComponent }
]);
]
});
fixture = TestBed.createComponent(exampleComponent);
exampleComponent = fixture.componentInstance;
});
it('should route to example/:id', inject([Router, Location], (router: Router, location: Location) => {
fixture.detectChanges();
exampleComponent.myId = FAKE_ID;
const LINK_BUTTON = fixture.debugElement.query(By.css('button'));
LINK_BUTTON.nativeElement.dispatchEvent(new Event('click'));
expect(location.path()).toEqual('/example/' + FAKE_ID);
});
});
There needs to be an outlet (<router-outlet>) for the DummyComponent. If the DummyComponent is a route being navigated to from the ExampleComponent, then the ExampleComponent should have the outlet. You also also need to add the ExampleComponent to the declarations`
#Component({
tempalte: `
<router-outlet></router-outlet>
<button (click)="nextPage">next page</button>
`
})
class ExampleComponent{}
declarations: [ ExampleComponent, DummyComponent ]
If you want to avoid having to set up this infrastructure just to test the route being navigated to, the better option might be to just mock the Router, and just check that the navigate method is called with the correct path.
beforeEach(()=>{
TestBed.configureTestingModule({
providers: [
{
provide: Router,
useValue: { navigate: jasmine.createSpy('navigate') }
}
]
})
})
With this, you don't need to configure an routing at all, as you're using a fake Router. Then in your test
it('should route to example/:id', inject([Router], (router: Router) => {
expect(router.navigate).toHaveBeenCalledWith(['/example', FAKE_ID]);
});
Related
Starting out with angular 2 after spending time with angular 1. Not having unit tested this much as it's more of a side project thing, I'm trying at least start out OK... I started with the example from AngularClass if that makes a difference.
Struggling in app.component.ts already, which contains my navigation bits. Relevant bits of the template here:
<nav class="navbar navbar-light bg-faded">
<div class="container">
<div class="nav navbar-nav">
<a class="navbar-brand" [routerLink]=" ['./'] ">Navbar</a>
<loading class="nav-item nav-link pull-xs-right" [visible]="user === null"></loading>
</div>
</div>
</nav>
<div class="container">
<main>
<router-outlet></router-outlet>
</main>
</div>
<footer>
<hr>
<div class="container">
</div>
</footer>
Component itself does not contain much:
import { Component, ViewEncapsulation } from '#angular/core';
import { AuthService } from './_services';
import { User } from './_models';
import { Loading } from './_components';
#Component({
selector: 'app',
encapsulation: ViewEncapsulation.None,
template: require('./app.component.html'),
styles: [
require('./app.style.css')
]
})
export class App {
user: User;
constructor(private auth: AuthService) {
}
ngOnInit() {
this.auth.getUser().subscribe(user => this.user = user);
}
}
All modules, components and routes are bootstrapped through the App module. Can post if required.
The test I'm having to write for it has me hooking up basically everything from the router (so it seems). First, [routerLink] is not a native attribute of 'a'. Ok, I fix it. Then:
Error in ./App class App - inline template:3:6 caused by: No provider for Router!
So, I hook up router, only to find:
Error in ./App class App - inline template:3:6 caused by: No provider for ActivatedRoute!
Which I added, to find out that:
Error in ./App class App - inline template:3:6 caused by: No provider for LocationStrategy!
By now, the test looks like:
import { inject, TestBed, async } from '#angular/core/testing';
import { AuthService } from './_services';
import { Router, RouterModule, ActivatedRoute } from '#angular/router';
import { AppModule } from './app.module';
// Load the implementations that should be tested
import { App } from './app.component';
import { Loading } from './_components';
describe('App', () => {
// provide our implementations or mocks to the dependency injector
beforeEach(() => TestBed.configureTestingModule({
declarations: [App, Loading],
imports: [RouterModule],
providers: [
{
provide: Router,
useClass: class {
navigate = jasmine.createSpy("navigate");
}
}, {
provide: AuthService,
useClass: class {
getAccount = jasmine.createSpy("getAccount");
isLoggedIn = jasmine.createSpy("isLoggedIn");
}
}, {
provide: ActivatedRoute,
useClass: class { }
}
]
}));
it('should exist', async(() => {
TestBed.compileComponents().then(() => {
const fixture = TestBed.createComponent(App);
// Access the dependency injected component instance
const controller = fixture.componentInstance;
expect(!!controller).toBe(true);
});
}));
});
I'm already mocking the inputs, this seems wrong to me. Am I missing something? Is there a smarter way of loading the whole app on a test, instead of bolting in every single dependency, all the time?
For testing, you should use the RouterTestingModule instead of the RouterModule. If you want to add routes you can use withRoutes
imports: [
RouterTestingModule.withRoutes(Routes) // same any normal route config
]
See Also
Angular 2 unit testing components with routerLink
Second half of this post for an idea of mock the ActivatedRoute. Sometimes you don't want the whole routing facility when unit testing. You can just mock the route.
I'm trying to get the basics of Angular2 test API and TestBed.compileComponents() is driving me nuts. Either I call it like this:
beforeEach( done => {
TestBed.configureTestingModule({
declarations: [MyComponent]
})
.compileComponents().then( () => {
fixture = TestBed.createComponent(MyComponent);
component = fixture.componentInstance();
});
done();
});
And then my component is undefined in my test (I believe since compileComponent is async, test is run before my var component gets a value)
Either like that (as describe in documentation):
beforeEach( async(() => {
TestBed.configureTestingModule({
declarations: [MyComponent]
}))
.compileComponents();
beforeEach( () => {
fixture = TestBed.createComponent(HomeComponent);
component = fixture.componentInstance();
});
But then I get the error: This test module uses the component HomeComponent which is using a "templateUrl", but they were never compiled. Please call "TestBed.compileComponents" before your test.
Can anybody help on this ?
Forget to say I use webpack and RC6
Try this:
describe('FooComponent', function () {
let fixture: ComponentFixture<FooComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [FooComponent]
});
fixture = TestBed.createComponent(FooComponent);
fixture.detectChanges();
});
You don't need asynchronicity here.
Try this:
beforeEach( async( () => {
TestBed.configureTestingModule({
declarations: [MyComponent]
})
.compileComponents().then( () => {
fixture = TestBed.createComponent(HomeComponent);
component = fixture.componentInstance();
});
}));
I faced the same issue. I think the problem is in your component template. If you use some other custom components, but didn't specify them in testing module, than Angular throws that error (of course very misleading).
So, you next options:
Specify all used components in TestBed configuration
As variation of that you may stub all components by corresponding mocks.
use NO_ERRORS_SCHEMA for shallow testing as described here https://angular.io/docs/ts/latest/guide/testing.html#shallow-component-test
Just to build on the answer by Zonkil I found to get this to work I had to actually set the fixture and create the component within the test. For example:
it('test the component renders',() => {
fixture = TestBed.createComponent(AppComponent);
comp = fixture.componentInstance;
fixture.detectChanges();
expect(comp).toBeDefined();
});
I'm trying to unit test a component that uses the Router, my code is in typescript. There are numerous recipies for injecting Router in a test spec, but none work for me, and some are not usable in the current version. I tried this:
beforeEach(() => {
addProviders([
MyComponent,
provideRouter([]),
provide(APP_BASE_HREF, { useValue: '/' }),
provide(ActivatedRoute, { useValue: {} })
]);
});
and got the error message
Error: Bootstrap at least one component before injecting Router.
When I try to mock the Router altogether:
class MockRouter {
public navigate() {}
}
beforeEach(() => {
addProviders([
MyComponent,
provide(Router, { useClass: MockRouter })
]);
});
the test suite stops altogether with the error message
TypeError: Attempting to configurable attribute of unconfigurable property.
In router_testing_module.d.ts, they suggest this:
beforeEach(() => {
configureModule({
modules: [RouterTestingModule],
providers: [provideRoutes(
[{path: '', component: BlankCmp}, {path: 'simple', component: SimpleCmp}])]
});
});
But the function configureModule does not seem to exist (yet? anymore?).
What can I do in this situation?
I have annoying error that probably by my mistake I cannot resolve.
I have on simple component which is actually nothing else than a top-bar element in my web application.
This component as you can see has only one dependency, the UserService and it uses it quite simply:
import { Component, OnInit } from '#angular/core';
import { MdButton } from '#angular2-material/button';
import { MdIcon , MdIconRegistry} from '#angular2-material/icon';
import { UserService } from '../services/index';
import { RouteConfig, ROUTER_DIRECTIVES, Router, ROUTER_PROVIDERS
} from '#angular/router-deprecated';
#Component({
moduleId: module.id,
selector: 'top-bar',
templateUrl: 'top-bar.component.html',
styleUrls: ['top-bar.component.css'],
providers: [MdIconRegistry, UserService, ROUTER_PROVIDERS],
directives: [MdButton, MdIcon, ROUTER_DIRECTIVES]
})
export class TopBarComponent implements OnInit {
constructor(private userService: UserService) {
this.userService = userService;
}
ngOnInit() {
}
/**
* Call UserService and logout() method
*/
logout() {
this.userService.logout();
}
}
As this service has also some dependencies (router etc) I had to provide them at the beforeEachProviders method as you can see:
import {
beforeEach,
beforeEachProviders,
describe,
expect,
it,
inject,
} from '#angular/core/testing';
import { TopBarComponent } from './top-bar.component';
import {
Router, RootRouter, RouteRegistry, ROUTER_PRIMARY_COMPONENT
} from '#angular/router-deprecated';
import { provide } from '#angular/core';
import { SpyLocation } from '#angular/common/testing';
import { UserService } from '../services/index';
describe('Component: TopBar', () => {
beforeEachProviders(() => [
RouteRegistry,
provide(Location, { useClass: SpyLocation }),
provide(ROUTER_PRIMARY_COMPONENT, { useValue: TopBarComponent }),
provide(Router, { useClass: RootRouter }),
UserService,
TopBarComponent
]);
it('should inject the component', inject([TopBarComponent],
(component: TopBarComponent) => {
expect(component).toBeTruthy();
}));
});
When I run the test though I get this error message:
Chrome 51.0.2704 (Mac OS X 10.11.5) Component: TopBar should inject the component FAILED
Error: No provider for Location! (TopBarComponent -> UserService -> Router -> Location)
Error: DI Exception[......]
First of all as you can see the Location provider is provided.
And secondary, why my test requires to provide (or inject) also the dependencies of the used into the tested component service?
For example if from the above test I remove the Router the even that my component doesn't use Router I'll get an error because the used service does. Then shouldn't I received the same error in the component and not only in the test?
UPDATE - CHANGE OF CODE & ERROR MESSAGE
I have manage to stop getting this error by changing my spec doe to this:
import {
beforeEach,
describe,
expect,
it,
} from '#angular/core/testing';
import { TopBarComponent } from './top-bar.component';
import { UserService } from '../services/index';
import {
Router
} from '#angular/router-deprecated';
import { Http } from '#angular/http';
import { AuthHttp } from 'angular2-jwt';
describe('Component: TopBar', () => {
let router: any = Router;
let authHttp: any = AuthHttp;
let http: any = Http;
let component: TopBarComponent;
let service: UserService = new UserService(router, authHttp, http);
beforeEach(() => {
component = new TopBarComponent(service);
});
it('logout function should work ', () => {
let logout = component.logout;
logout();
expect(localStorage.getItem('token')).toBe(null);
});
});
But know I'm getting this error from my component:
TypeError: Cannot read property 'userService' of undefined
The mentioned error is on this function:
logout() {
this.userService.logout();
}
of my component but this is only on the test. In app it works normally. The function cannot reach constructor's parameter for some reason in my test.
kind of stack here...
By your code I understand that you are trying to test the topbar component.
Top bar component has a dependency on UserService.
So to answer your question, Angular does dependency injection when you run your application because all the providers are configured in the module file. But when you try to test the code in spec file you have to configure the testbed with all providers, components in the beforeEach method that are going to be used and angular leaves all the responsibility of resolving the dependency to the user as testbed acts as environment to run your code.
In your code you can do something like this
let service: UserService;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [UserService, any other service on which user service is dependent] });
});
Here you can see TestBed.configureTestingModule method creates a dummy module to aid in running ur test case.
My suggestion will be create a mock UserService which doesn't have any other dependencies like the original one and assign it in the provider
Something like this
export MockUserService {
Put all essential methods stub here.
}
let service: UserService;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [provide: UserService, useClass: MockUserService] });
});
Then just test the topBar component's usecases.
Try creating the object of service inside beforeEach using TestBed.get(UserService). This code will automatically resolve the dependencies and create that object for use.
Remove '= new UserService(router, authHttp, http);' from 'let service: UserService = new UserService(router, authHttp, http);'
How do I access the current model? I am aware of application.__container_.lookup but I understand this is a bit of a hack.
import Ember from 'ember';
import { module, test } from 'qunit';
import startApp from 'myapp/tests/helpers/start-app';
let application;
module('Acceptance | booking/edit', {
beforeEach: function() {
application = startApp();
},
afterEach: function() {
Ember.run(application, 'destroy');
}
});
test('visiting /booking/edit', function(assert) {
visit('/booking/1');
//At this point I would like to access the model returned from the route model hook.
andThen(function() {
assert.equal(currentURL(), '/booking/1');
});
});
Sample Route excerpt.
this.route('booking', { path:'/booking' }, function() {
this.route('edit', { path:'/:booking_id' }, function() {
this.route('account', { path:'/account' });
...
});
...
});
You should be able to use moduleFor and then within the test you can use this.subject() to access the controller.
moduleFor('controller:bookingsEdit', 'Bookings Edit Controller');
If moduleFor is undefined. Then import moduleFor import {moduleFor} from 'ember-qunit';
and then within the test you can use this.subject() to access the controller
moduleFor(fullName [, description [, callbacks]])
fullName: (String) - The full name of the unit, ie
controller:application, route:index.
description: (String) optional - The description of the module
callbacks: (Object) optional - Normal QUnit callbacks (setup and
teardown), with addition to needs, which allows you specify the other
units the tests will need.
http://guides.emberjs.com/v1.10.0/testing/testing-controllers/
https://github.com/rwjblue/ember-qunit