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
}
}
Related
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.
I'm new to angular 2 and I have some problems testing my code. I use the jasmine testing framework and the karma test runner to test my app.
I have a component (called GroupDetailsComponent) that I want to test. This component uses two services (GroupService & TagelerServie, both have CRUD methods to talk to an API) and some pipes in the html file. My component looks like this:
import 'rxjs/add/operator/switchMap';
import { Component, Input, OnInit } from '#angular/core';
import { Tageler } from '../../tagelers/tageler';
import { TagelerService } from '../../tagelers/tageler.service';
import { Params, ActivatedRoute } from '#angular/router';
import { GroupService} from "../group.service";
import { Group } from '../group';
#Component({
selector: 'app-group-details',
templateUrl: 'group-details.component.html',
styleUrls: ['group-details.component.css'],
})
export class GroupDetailsComponent implements OnInit {
#Input()
tageler: Tageler;
tagelers: Tageler[];
group: Group;
constructor(
private route: ActivatedRoute,
private groupService: GroupService,
private tagelerService: TagelerService) {
}
ngOnInit() {
console.log("Init Details");
this.route.params
.switchMap((params: Params) => this.groupService.getGroup(params['id']))
.subscribe(group => this.group = group);
this.tagelerService
.getTagelers()
.then((tagelers: Tageler[]) => {
// some code
}
return tageler;
});
});
}
}
And the test file looks like this:
import { async, ComponentFixture, TestBed } from '#angular/core/testing';
import { GroupDetailsComponent } from './group-details.component';
import { FilterTagelerByGroupPipe } from '../../pipes/filterTagelerByGroup.pipe';
import { SameDateTagelerPipe } from '../../pipes/sameDateTageler.pipe';
import { CurrentTagelerPipe } from '../../pipes/currentTageler.pipe';
import { NextTagelerPipe } from '../../pipes/nextTageler.pipe';
import { RouterTestingModule } from '#angular/router/testing';
import { GroupService } from '../group.service';
import { TagelerService } from '../../tagelers/tageler.service';
describe('GroupDetailsComponent', () => {
let component: GroupDetailsComponent;
let fixture: ComponentFixture<GroupDetailsComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [
GroupDetailsComponent,
FilterTagelerByGroupPipe,
SameDateTagelerPipe,
CurrentTagelerPipe,
NextTagelerPipe, ],
imports: [ RouterTestingModule ],
providers: [{provide: GroupService}, {provide: TagelerService}],
})
fixture = TestBed.createComponent(GroupDetailsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
class MockGroupService {
getGroups(): Array<Group> {
let toReturn: Array<Group> = [];
toReturn.push(new Group('Trupp', 'Gruppe 1'));
return toReturn;
};
}
it('should create component', () => {
expect(component).toBeDefined();
});
});
I read the angular 2 documentation about testing and a lot of blogs, but I still don't really understand how to test a component that uses services and pipes. When I start the test runner, the test 'should create component' fails and I get the message that my component is not defined (but I don't understand why). I also don't understand how I have to inject the services and pipes. How do I mock them the right way?
I hope that someone can give me helpful advice!
Ramona
You can use spyOn to fake the call in jasmine.
spyOn(yourService, 'method').and.returnValue($q.resolve(yourState));
Here I'm new to angular 2 test cases with jasmine + karma and I'm following this testing guide I tried to write test case but unable to write correctly and getting routing error as test cases are going to server while running, although it should not.
There is a question similar to this but didn't help me to solve the problem. Please guide me.
Here is my component
import { Component, OnInit, ViewChild } from '#angular/core';
import { Router, ActivatedRoute } from '#angular/router';
import { Checklist } from './checklist';
import { Job } from '../+job/job';
import { OriginalService } from './original.service';
import { UserService } from '../core/user/user.service';
import { ModalDirective } from 'ng2-bootstrap/ng2-bootstrap';
#Component({
selector: 'app-selector',
templateUrl: './original.component.html',
providers: [OriginalService]
})
export class OriginalComponent implements OnInit {
items = []
job: Job;
checklist: Checklist;
user: any;
shopId: any;
jobId: any;
constructor ( private orgService: OriginalService, private route: ActivatedRoute, private router: Router, userService: UserService) {
this.user = userService.user();
}
ngOnInit() {
this.route.params
.map(params => params['job_id'])
.subscribe(
job_id => this.jobId = job_id
);
this.getChecklist();
this.getJob();
}
getChecklist() {
this.orgService.getChecklists({
jobId: this.jobId
})
.then(checklist=> {
this.gotJobdata = true;
this.checklist = checklist.response})
.catch(error => this.error = error);
}
getJob(){
this.orgService.getJob({
jobId: this.jobId
})
.then(job => this.job = job.response)
.catch(error => this.error = error);
}
}
Here's my service
import { Injectable, ViewChildren } from '#angular/core';
import { Http, Response, Headers, RequestOptions } from '#angular/http';
import { environment } from '../../environments/environment'
import 'rxjs/add/operator/toPromise';
import { Checklist } from './checklist';
import { Job } from '../+job/job';
import { UserService } from '../core/user/user.service';
#Injectable()
export class OriginalService {
shopId: any;
constructor(private http: Http, private userService: UserService ) {
this.shopId = userService.shopId();
}
getChecklists(svc): Promise<Checklist> {
return this.http.get(environment.apiUrl + this.shopId + '/jobs/' + svc.jobId + '/checklists_path/' + 'check_item.json')
.toPromise()
.then(response => response.json())
.catch(this.handleError);
}
getJob(svc): Promise<Job> {
return this.http.get(return environment.apiUrl + this.shopId + '/jobs/' + svc.jobId + '.json')
.toPromise()
.then(response => response.json())
.catch(this.handleError);
}
}
Here's the spec what I've tried:
import { async, ComponentFixture, TestBed } from '#angular/core/testing';
import { SharedModule } from '../shared/shared.module';
import { ModalModule } from 'ng2-bootstrap/ng2-bootstrap';
import { HttpModule } from '#angular/http';
import { Router, ActivatedRoute, Params } from '#angular/router';
import { ComponentLoaderFactory } from 'ng2-bootstrap/component-loader';
import { Subject } from 'rxjs/Subject';
import { UserService } from '../core/user/user.service';
import { OriginalService } from './original.service';
import { OriginalComponent } from './original.component';
describe('OriginalComponent', () => {
let component: OriginalComponent;
let fixture: ComponentFixture<OriginalComponent>;
let params: Subject<Params>;
let userService, orgService;
let userServiceStub = {
isLoggedIn: true,
user: { name: 'Test User'}
};
beforeEach(async(() => {
params = new Subject<Params>();
TestBed.configureTestingModule({
imports: [ ModalModule.forRoot(), SharedModule, HttpModule ],
declarations: [ OriginalComponent ],
providers: [ OriginalService, UserService, ComponentLoaderFactory,
{ provide: Router, useValue: userServiceStub }, {provide: ActivatedRoute, useValue: { params: params }} ]
})
.compileComponents();
})
);
beforeEach(() => {
fixture = TestBed.createComponent(ChecklistComponent);
component = fixture.componentInstance;
fixture.detectChanges();
it('should create', () => {
expect(component).toBeTruthy();
});
});
Please correct me how to properly use routing in test cases or stubs where I'm doing wrong.
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 ]
})
It's kinda confusing as I cannot find anything related in docs of Angular 2.0 and Router-deprecated (yes I have to use it still in my project).
My service looks like this:
import { Injectable } from '#angular/core';
import { Http, Headers } from '#angular/http';
import { AuthHttp , JwtHelper } from 'angular2-jwt';
import { Router } from '#angular/router-deprecated';
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;
}
login(v) {
this.http.post(myUrl)
.subscribe(
data => this.loginSuccess(data),
err => this.loginFailure(err)
);
}
}
And my test like this (don't really care about the 'it' part for now):
import { Http } from '#angular/http';
import { AuthHttp, JwtHelper } from 'angular2-jwt';
import { Router } from '#angular/router-deprecated';
import {
beforeEach, beforeEachProviders,
describe, xdescribe,
expect, it, xit,
async, inject
} from '#angular/core/testing';
import { UserService } from './user.service';
describe('User Service', () => {
let service;
beforeEachProviders(() => [
Router,
AuthHttp,
Http,
UserService
]);
beforeEach(inject([
Router,
AuthHttp,
Http,
UserService], s => {
service = s;
}));
it('Should have a login method', () => {
expect(service.login()).toBeTruthy();
});
});
When I run the test I get this error: (btw I'm using angular-cli)
Error: Cannot resolve all parameters for 'Router'(RouteRegistry, Router, ?, Router). Make sure that all the parameters are decorated with Inject or have valid type annotations and that 'Router' is decorated with Injectable.
Am I so wrong here?
After lots and lots of searching around I found out that I was injecting wrongly the providers.
Based on this GREAT article I manage to solve my problem by changing my service to this:
import { Http } from '#angular/http';
import { provide } from '#angular/core';
import { SpyLocation } from '#angular/common/testing';
import { AuthHttp, JwtHelper } from 'angular2-jwt';
import {
Router, RootRouter, RouteRegistry, ROUTER_PRIMARY_COMPONENT
} from '#angular/router-deprecated';
import {
beforeEach, beforeEachProviders,
describe, xdescribe,
expect, it, xit,
async, inject
} from '#angular/core/testing';
import { UserService } from './user.service';
describe('User Service', () => {
let service = UserService.prototype;
beforeEachProviders(() => [
RouteRegistry,
provide(Location, {useClass: SpyLocation}),
provide(ROUTER_PRIMARY_COMPONENT, {useValue: UserService}),
provide(Router, {useClass: RootRouter}),
AuthHttp,
Http,
UserService
]);
it('Should have a login method', () => {
expect(service.login).toBeTruthy();
});
});