Ionic 2 NavController unit testing - unit-testing

I'm kinda new to unit testing in Angular 2 and Ionic 2.
I am trying to test the login() method below
export class LoginPage {
constructor(public navCtrl: NavController) {}
login() {
this.navCtrl.setRoot(TabsPage);
}
}
With the following test
import { ComponentFixture, async } from '#angular/core/testing';
import { TestUtils } from '../../test';
import { LoginPage } from './login';
import { TabsPage } from '../tabs/tabs';
let fixture: ComponentFixture<LoginPage> = null;
let instance: any = null;
describe('Login Page', () => {
beforeEach(async(() => TestUtils.beforeEachCompiler([LoginPage]).then(compiled => {
fixture = compiled.fixture;
instance = compiled.instance;
})));
it('changes root nav to TabsPage on login()', () => {
spyOn(instance.navCtrl, 'setRoot');
instance.login();
expect(instance.navCtrl.setRoot).toHaveBeenCalledWith(TabsPage);
});
});
But I get the following error
Error: <spyOn> : setRoot() method does not exist
I followed this tutorial for set up.
I must be missing something. Is instance.navCtrl the right thing to be spying on?

It ended being a typo. I was using useValue instead of useClass in the provide, so the setRoot method had to be accessed through prototype.
I had
{provide: NavController, useValue: NavMock}
Instead of
{provide: NavController, useClass: NavMock}

Related

Ionic async unit test with LoadingController

I have an Ionic Angular Application and I am trying to write a simple unit test for the service method. The method displays a Loading spinner and then returns true.
See the code below:
Service.specs.ts
import {
TestBed,
ComponentFixture,
inject,
fakeAsync,
tick,
flushMicrotasks,
} from '#angular/core/testing'; import { GeneralmethodsService } from './generalmethods.service';
import { DomSanitizer } from '#angular/platform-browser';
import { ActivatedRoute, Router } from '#angular/router';
import { HTTP } from '#ionic-native/http/ngx';
import { Network } from '#ionic-native/network';
import { AlertController, LoadingController, ModalController, ToastController } from '#ionic/angular';
import { HandleNetworkService } from './handle-network.service';
import { TmmserviceService } from './tmmservice.service';
import { TranslateService } from '#ngx-translate/core';
describe('GeneralmethodsService xxx', () => {
let modalController: ModalController;
let tmmserviceServiceNatice: TmmserviceService;
let loadingController: LoadingController;
let router: Router;
let toastController: ToastController;
let translate: TranslateService;
let Service: GeneralmethodsService;
beforeEach(async () => {
TestBed.configureTestingModule({
providers: [
GeneralmethodsService,
{
provide: LoadingController,
useValue: {
create: () => Promise.resolve(),
dismiss: () => Promise.resolve()
}
}
]
}).compileComponents();
Service = new GeneralmethodsService(
modalController,
router,
loadingController,
tmmserviceServiceNatice,
toastController,
translate
);
});
it('test testPist', fakeAsync(() => {
return Service.testPist().then(async (data) => {
expect(data).toBe(true);
flushMicrotasks();
});
}));
});
Below you can see the method implementation in the service
Service.ts
async testPist(){
let loading = await this.loadingController.create();
await loading.present();
loading.dismiss();
return true;
}
This is the error I'm getting:
Error
Error: Uncaught (in promise): TypeError: Cannot read properties of undefined (reading 'create')
TypeError: Cannot read properties of undefined (reading 'create')
Can someone tell me what am I doing wrong?
Thanks in advance
Yes, because the loadingController is undefined.
Follow the comments with !!
TestBed.configureTestingModule({
providers: [
GeneralmethodsService,
{
provide: LoadingController,
// !! You defined loading controller here in the TestBed module
useValue: {
create: () => Promise.resolve(),
dismiss: () => Promise.resolve()
}
}
]
}).compileComponents();
// Service = new GeneralmethodsService(
// modalController,
// router,
// !! at this point loadingController is undefined
// loadingController,
// tmmserviceServiceNatice,
// toastController,
// translate
// );
// !! comment out the above and get a handle on the service using the TestBed
Service = TestBed.inject(GeneralMethodsService);
});
Doing the above changes should hopefully get you unblocked. Since you have a TestBed.configureTestingModule and it is configured, we can use that to get a handle on the service under test.

Unit testing Angular 2 authGuard; spy method is not being called

I'm trying to unit test my auth guard service. From this answer I was able to get this far, but now when I run the unit test for this, it says Expected spy navigate to have been called.
How to I get my spied router to be used as this.router in the service?
auth-guard.service.ts
import { Injectable } from '#angular/core';
import { Router, CanActivate } from '#angular/router';
#Injectable()
export class AuthGuardService {
constructor(private router:Router) { }
public canActivate() {
const authToken = localStorage.getItem('auth-token');
const tokenExp = localStorage.getItem('auth-token-exp');
const hasAuth = (authToken && tokenExp);
if(hasAuth && Date.now() < +tokenExp){
return true;
}
this.router.navigate(['/login']);
return false;
}
}
auth-guard.service.spec.ts
import { TestBed, async, inject } from '#angular/core/testing';
import { RouterTestingModule } from '#angular/router/testing';
import { AuthGuardService } from './auth-guard.service';
describe('AuthGuardService', () => {
let service:AuthGuardService = null;
let router = {
navigate: jasmine.createSpy('navigate')
};
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
AuthGuardService,
{provide:RouterTestingModule, useValue:router}
],
imports: [RouterTestingModule]
});
});
beforeEach(inject([AuthGuardService], (agService:AuthGuardService) => {
service = agService;
}));
it('checks if a user is valid', () => {
expect(service.canActivate()).toBeFalsy();
expect(router.navigate).toHaveBeenCalled();
});
});
Replacing RouterTestingModule with Router like in the example answer throws Unexpected value 'undefined' imported by the module 'DynamicTestModule'.
Instead of stubbing Router, use dependency injection and spy on the router.navigate() method:
import { TestBed, async, inject } from '#angular/core/testing';
import { RouterTestingModule } from '#angular/router/testing';
import { Router } from '#angular/router';
import { AuthGuardService } from './auth-guard.service';
describe('AuthGuardService', () => {
beforeEach(() => {
TestBed.configureTestingModule({
providers: [AuthGuardService],
imports: [RouterTestingModule]
});
});
it('checks if a user is valid',
// inject your guard service AND Router
async(inject([AuthGuardService, Router], (auth, router) => {
// add a spy
spyOn(router, 'navigate');
expect(auth.canActivate()).toBeFalsy();
expect(router.navigate).toHaveBeenCalled();
})
));
});
https://plnkr.co/edit/GNjeJSQJkoelIa9AqqPp?p=preview
For this test, you can use the ReflectiveInjector to resolve and create your auth-gaurd service object with dependencies.
But instead of passing the actual Router dependency, provide your own Router class (RouterStub) that has a navigate function. Then spy on the injected Stub to check if navigate was called.
import {AuthGuardService} from './auth-guard.service';
import {ReflectiveInjector} from '#angular/core';
import {Router} from '#angular/router';
describe('AuthGuardService', () => {
let service;
let router;
beforeEach(() => {
let injector = ReflectiveInjector.resolveAndCreate([
AuthGuardService,
{provide: Router, useClass: RouterStub}
]);
service = injector.get(AuthGuardService);
router = injector.get(Router);
});
it('checks if a user is valid', () => {
let spyNavigation = spyOn(router, 'navigate');
expect(service.canActivate()).toBeFalsy();
expect(spyNavigation).toHaveBeenCalled();
expect(spyNavigation).toHaveBeenCalledWith(['/login']);
});
});
class RouterStub {
navigate(routes: string[]) {
//do nothing
}
}

Angular 2 Jasmine Can't bind to 'routerLink' since it isn't a known property of 'a'

I'm creating a unit test for my Navbar Component and I'm getting an error:
Can't bind to 'routerLink' since it isn't a known property of 'a'
Navbar Component TS
import { Component } from '#angular/core';
import { Router } from '#angular/router';
import { NavActiveService } from '../../../services/navactive.service';
import { GlobalEventsManager } from '../../../services/GlobalEventsManager';
#Component({
moduleId: module.id,
selector: 'my-navbar',
templateUrl: 'navbar.component.html',
styleUrls:['navbar.component.css'],
providers: [NavActiveService]
})
export class NavComponent {
showNavBar: boolean = true;
constructor(private router: Router,
private navactiveservice:NavActiveService,
private globalEventsManager: GlobalEventsManager){
this.globalEventsManager.showNavBar.subscribe((mode:boolean)=>{
this.showNavBar = mode;
});
}
}
Navbar Component Spec
import { ComponentFixture, TestBed, async } from '#angular/core/testing';
import { NavComponent } from './navbar.component';
import { DebugElement } from '#angular/core';
import { By } from '#angular/platform-browser';
import { Router } from '#angular/router';
export function main() {
describe('Navbar component', () => {
let de: DebugElement;
let comp: NavComponent;
let fixture: ComponentFixture<NavComponent>;
let router: Router;
// preparing module for testing
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [NavComponent],
}).compileComponents().then(() => {
fixture = TestBed.createComponent(NavComponent);
comp = fixture.componentInstance;
de = fixture.debugElement.query(By.css('p'));
});
}));
it('should create component', () => expect(comp).toBeDefined());
/* it('should have expected <p> text', () => {
fixture.detectChanges();
const h1 = de.nativeElement;
expect(h1.innerText).toMatch(" ");
});*/
});
}
I realize that I need to add router as a spy, but if I add it as a SpyObj and declare it as a provider I get the same error.
Is there a better way for me to add fix this error?
EDIT: Working Unit Test
Built this unit test based on the answer:
import { ComponentFixture, TestBed, async } from '#angular/core/testing';
import { NavComponent } from './navbar.component';
import { DebugElement } from '#angular/core';
import { By } from '#angular/platform-browser';
import { RouterLinkStubDirective, RouterOutletStubComponent } from '../../../../test/router-stubs';
import { Router } from '#angular/router';
import { GlobalEventsManager } from '../../../services/GlobalEventsManager';
import { RouterModule } from '#angular/router';
import { SharedModule } from '../shared.module';
export function main() {
let comp: NavComponent;
let fixture: ComponentFixture<NavComponent>;
let mockRouter:any;
class MockRouter {
//noinspection TypeScriptUnresolvedFunction
navigate = jasmine.createSpy('navigate');
}
describe('Navbar Componenet', () => {
beforeEach( async(() => {
mockRouter = new MockRouter();
TestBed.configureTestingModule({
imports: [ SharedModule ]
})
// Get rid of app's Router configuration otherwise many failures.
// Doing so removes Router declarations; add the Router stubs
.overrideModule(SharedModule, {
remove: {
imports: [ RouterModule ],
},
add: {
declarations: [ RouterLinkStubDirective, RouterOutletStubComponent ],
providers: [ { provide: Router, useValue: mockRouter }, GlobalEventsManager ],
}
})
.compileComponents()
.then(() => {
fixture = TestBed.createComponent(NavComponent);
comp = fixture.componentInstance;
});
}));
tests();
});
function tests() {
let links: RouterLinkStubDirective[];
let linkDes: DebugElement[];
beforeEach(() => {
// trigger initial data binding
fixture.detectChanges();
// find DebugElements with an attached RouterLinkStubDirective
linkDes = fixture.debugElement
.queryAll(By.directive(RouterLinkStubDirective));
// get the attached link directive instances using the DebugElement injectors
links = linkDes
.map(de => de.injector.get(RouterLinkStubDirective) as RouterLinkStubDirective);
});
it('can instantiate it', () => {
expect(comp).not.toBeNull();
});
it('can get RouterLinks from template', () => {
expect(links.length).toBe(5, 'should have 5 links');
expect(links[0].linkParams).toBe( '/', '1st link should go to Home');
expect(links[1].linkParams).toBe('/', '2nd link should go to Home');
expect(links[2].linkParams).toBe('/upload', '3rd link should go to Upload');
expect(links[3].linkParams).toBe('/about', '4th link should to to About');
expect(links[4].linkParams).toBe('/login', '5th link should go to Logout');
});
it('can click Home link in template', () => {
const uploadLinkDe = linkDes[1];
const uploadLink = links[1];
expect(uploadLink.navigatedTo).toBeNull('link should not have navigated yet');
uploadLinkDe.triggerEventHandler('click', null);
fixture.detectChanges();
expect(uploadLink.navigatedTo).toBe('/');
});
it('can click upload link in template', () => {
const uploadLinkDe = linkDes[2];
const uploadLink = links[2];
expect(uploadLink.navigatedTo).toBeNull('link should not have navigated yet');
uploadLinkDe.triggerEventHandler('click', null);
fixture.detectChanges();
expect(uploadLink.navigatedTo).toBe('/upload');
});
it('can click about link in template', () => {
const uploadLinkDe = linkDes[3];
const uploadLink = links[3];
expect(uploadLink.navigatedTo).toBeNull('link should not have navigated yet');
uploadLinkDe.triggerEventHandler('click', null);
fixture.detectChanges();
expect(uploadLink.navigatedTo).toBe('/about');
});
it('can click logout link in template', () => {
const uploadLinkDe = linkDes[4];
const uploadLink = links[4];
expect(uploadLink.navigatedTo).toBeNull('link should not have navigated yet');
uploadLinkDe.triggerEventHandler('click', null);
fixture.detectChanges();
expect(uploadLink.navigatedTo).toBe('/login');
});
}
}
Just import RouterTestingModule in TestBed.configureTestingModule of your components spec.ts file
Eg:
import { RouterTestingModule } from '#angular/router/testing';
TestBed.configureTestingModule({
imports: [RouterTestingModule],
declarations: [ ComponentHeaderComponent ]
})
The Angular Testing docs address this by using RouterLinkDirectiveStub and RouterOutletStubComponent so that routerLink is a known property of <a>.
Basically it says that using RouterOutletStubComponent is a safe way to test routerLinks without all the complications and errors of using the real RouterOutlet. Your project needs to know it exists so it doesn't throw errors but it doesn't need to actually do anything in this case.
The RouterLinkDirectiveStub enables you to click on <a> links with routerLink directive and get just enough information to test that it is being clicked (navigatedTo) and going to the correct route (linkParams). Any more functionality than that and you really aren't testing your component in isolation any more.
Take a look at their Tests Demo in app/app.component.spec.ts. Grab the testing/router-link-directive-stub.ts and add to your project. Then you will inject the 2 stubbed items into your TestBed declarations.
If you want only isolated test and DO NOT CARE about template,you can add NO_ERRORS_SCHEMA. This tells Angular not to show error if it encounters any unknown attribute or element in HTML
Eg:
TestBed.configureTestingModule({
declarations: [ ComponentHeaderComponent ],
schemas: [ NO_ERRORS_SCHEMA ]
})

TypeError: Cannot read property 'injector' of null after upgrading to Angular 2.0 final

After upgrading from Angular 2 RC5 to 2.0 Final, I'm seeing the following errors when running my tests. I'm not sure what the problem is.
TypeError: Cannot read property 'injector' of null
at TestBed._createCompilerAndModule (webpack:///Users/mraible/dev/ng2-demo/~/#angular/core/testing/test_bed.js:247:0 <- src/test.ts:20777:44)
at TestBed._initIfNeeded (webpack:///Users/mraible/dev/ng2-demo/~/#angular/core/testing/test_bed.js:213:0 <- src/test.ts:20743:39)
at TestBed.createComponent (webpack:///Users/mraible/dev/ng2-demo/~/#angular/core/testing/test_bed.js:297:0 <- src/test.ts:20827:14)
Here's an example of one of my tests:
import { MockSearchService } from '../shared/search/mocks/search.service';
import { EditComponent } from './edit.component';
import { TestBed } from '#angular/core/testing/test_bed';
import { SearchService } from '../shared/search/search.service';
import { MockRouter, MockActivatedRoute } from '../shared/search/mocks/routes';
import { ActivatedRoute, Router } from '#angular/router';
import { FormsModule } from '#angular/forms';
describe('Component: Edit', () => {
let mockSearchService: MockSearchService;
let mockActivatedRoute: MockActivatedRoute;
let mockRouter: MockRouter;
beforeEach(() => {
mockSearchService = new MockSearchService();
mockActivatedRoute = new MockActivatedRoute({'id': 1});
mockRouter = new MockRouter();
TestBed.configureTestingModule({
declarations: [EditComponent],
providers: [
{provide: SearchService, useValue: mockSearchService},
{provide: ActivatedRoute, useValue: mockActivatedRoute},
{provide: Router, useValue: mockRouter}
],
imports: [FormsModule]
});
});
it('should fetch a single record', () => {
const fixture = TestBed.createComponent(EditComponent);
let person = {name: 'Emmanuel Sanders', address: {city: 'Denver'}};
mockSearchService.setResponse(person);
fixture.detectChanges();
// verify service was called
expect(mockSearchService.getByIdSpy).toHaveBeenCalledWith(1);
// verify data was set on component when initialized
let editComponent = fixture.debugElement.componentInstance;
expect(editComponent.editAddress.city).toBe('Denver');
// verify HTML renders as expected
let compiled = fixture.debugElement.nativeElement;
expect(compiled.querySelector('h3').innerHTML).toBe('Emmanuel Sanders');
});
});
For a pull request that demonstrates this issue, please see GitHub: https://github.com/mraible/ng2-demo/pull/4
Changing import { TestBed } from '#angular/core/testing/test_bed'; to import { TestBed } from '#angular/core/testing'; solved this problem for me.

Angular 2 RC4: Unit test component that has dependency injection a service that has its own

As Angular team is constantly upgrading/deprecating stuff in Angular 2 RC versions I encountered this problem.
I have a component that has a Dependency Injection (DI), which is actually a service (UserService in this case). This UserService of course has some DIs of its own. After updating to the latest RC4 of Angular 2 I realised that I cannot create similar tests any more.
So as the docs are not mentioning something relative here's my code (simplified for this question).
My component:
import { Component } from '#angular/core';
import { MdButton } from '#angular2-material/button';
import {
MdIcon,
MdIconRegistry
} from '#angular2-material/icon';
import { UserService } from '../../services/index';
#Component({
moduleId: module.id,
selector: 'logout-button',
templateUrl: 'logout-button.component.html',
styleUrls: ['logout-button.component.css'],
providers: [MdIconRegistry, UserService],
directives: [MdButton, MdIcon]
})
export class LogoutButtonComponent {
constructor(public userService: UserService) {}
/**
* Call UserService and logout() method
*/
logout() {
this.userService.logout();
}
}
Component's DI, UserService whic as you can see has some DIs (Router, AuthHttp & Http):
import { Injectable } from '#angular/core';
import {
Http,
Headers
} from '#angular/http';
import {
AuthHttp,
JwtHelper
} from 'angular2-jwt';
import { Router } from '#angular/router';
import { UMS } from '../common/index';
#Injectable()
export class UserService {
constructor(
private router: Router,
private authHttp: AuthHttp,
private http: Http) {
this.router = router;
this.authHttp = authHttp;
this.http = http;
}
/**
* Logs out user
*/
public logout() {
this.authHttp.get(UMS.url + UMS.apis.logout)
.subscribe(
data => this.logoutSuccess(),
err => this.logoutSuccess()
);
}
}
And here's the test for the component:
import { By } from '#angular/platform-browser';
import { DebugElement } from '#angular/core';
import {
beforeEach,
beforeEachProviders,
describe,
expect,
it,
inject,
fakeAsync,
TestComponentBuilder
} from '#angular/core/testing';
import { AuthHttp } from 'angular2-jwt';
import { Router } from '#angular/router';
import { Http } from '#angular/http';
import { LogoutButtonComponent } from './logout-button.component';
import { UserService } from '../../services/index';
describe('Component: LogoutButtonComponent', () => {
beforeEachProviders(() => [
LogoutButtonComponent,
UserService
]);
it('should inject UserService', inject([LogoutButtonComponent],
(component: LogoutButtonComponent) => {
expect(component).toBeTruthy();
}));
});
Don't worry about the (it) for now.
As you can see I;m adding the related providers on the beforeEachProviders.
In this case I'm getting an error when I run the tests:
Error: No provider for Router! (LogoutButtonComponent -> UserService -> Router)
Which is expected let's say.
So in order to don't get those errors I'm adding the service's DIs in the providers also:
beforeEachProviders(() => [
LogoutButtonComponent,
Router,
AuthHttp,
Http,
UserService
]);
But now I'm, getting this error:
Error: Cannot resolve all parameters for 'Router'(?, ?, ?, ?, ?, ?, ?). Make sure that all the parameters are decorated with Inject or have valid type annotations and that 'Router' is decorated with Injectable.
I'm really trying to figure out what's happening so I found some related answers here but ALL are outdated and covers the old router-deprecated or angular2/router but both are deprecated and are not covering this case.
Would love some help on this and maybe some resources as I cannot find anything related to the latest version of Router: "#angular/router": "3.0.0-beta.2", and RC4.
Thanks
UPDATE!
I manage to bypass the two errors above and now I can access the component. Here's the description code:
describe('Component: LogoutButtonComponent', () => {
let component: LogoutButtonComponent;
let router: any = Router;
let authHttp: any = AuthHttp;
let http: any = Http;
let service: any = new UserService(router, authHttp, http);
beforeEachProviders(() => [
LogoutButtonComponent
]);
beforeEach(() => {
component = new LogoutButtonComponent(service);
});
it('should inject UserService', () => {
expect(component.userService).toBeTruthy();
});
it('should logout user', () => {
localStorage.setItem('token', 'FOO');
component.logout();
expect(localStorage.getItem('token')).toBeUndefined();
});
});
But it seems that even that the DI service is injected and accessible the DIs of the service are not. So now I get this error:
TypeError: this.authHttp.get is not a function
Any ideas?
It looks like you were experiencing a dependencies loop problem, because your UserSerivce also need inject AuthHttp, Http, etc... it really will be disturb once if you need test your component.
My way is just create a mock UserSerivce and return the expect mocked value through UserService.logout() method, because you don't have to know what really happened in UserService, all you need is just a return value:
let MockUserService = {
logout() {
// return some value you need
}
}
Then, in test suite:
import { provide } from '#angular/core'
beforeEachProviders(() => [
provide(UserService, {useClass: MockUserService})
])
... detail test code here
I hope this works for you.
And here is a post that helps me a lot:
https://developers.livechatinc.com/blog/testing-angular-2-apps-dependency-injection-and-components/
With RC4, some workarounds are needed to use http in a test. See also this issue (should be fixed in RC5):
https://github.com/angular/angular/issues/9294
Adding this to my unit-tests.html fixed it for me:
System.import('#angular/platform-browser/src/browser/browser_adapter').then(function(browser_adapter) {
browser_adapter.BrowserDomAdapter.makeCurrent();
})