How to mock service? - unit-testing

I have login function inside my LoginComponent:
login() {
this.loading = true;
this.subscription = this.authenticationService.login(this.model.username, this.model.password)
.subscribe(result => {
this.em.changeNav(1);
this.loading = false;
this.Auth.setToken(result);
this.router.navigate(['/code']);
this.subscription.unsubscribe();
},
err => {
this.error = JSON.parse(err._body).error;
this.loading = false;
});
}
this.authenticationService.login is the service which send http request to api...
Here is the test:
it('should login', fakeAsync(() => {
spyOn(component, 'login');
let button = fixture.debugElement.nativeElement.querySelector('button');
button.click();
//CHECK IF LOGIN FUNCTION CALLED
fixture.whenStable().then(() => {
expect(component.login).toHaveBeenCalled();
})
}));
How can I mock this.authenticationService.login service and assert things in subscribe method?
EDIT
Test:
import { async, ComponentFixture, TestBed, fakeAsync, tick, inject } from '#angular/core/testing';
import { By } from '#angular/platform-browser';
import { DebugElement } from '#angular/core';
import { FormsModule, ReactiveFormsModule } from '#angular/forms';
import { RouterTestingModule } from '#angular/router/testing';
import {Router} from '#angular/router';
import { Http, Request, RequestOptionsArgs, Response, XHRBackend, RequestOptions, ConnectionBackend, Headers, HttpModule, BaseRequestOptions } from '#angular/http';
import {LoginService} from './login.service';
import {
MockBackend,
MockConnection
} from '#angular/http/testing';
import {EmitterService} from '../emitter.service';
import {AuthTokenService} from '../auth-token.service';
import { LoginComponent } from './login.component';
import {Observable} from 'rxjs';
describe('LoginComponent', () => {
let backend: MockBackend;
let service: LoginService;
let component: LoginComponent;
let fixture: ComponentFixture<LoginComponent>;
beforeEach(async(() => {
class LoginServiceStub {
login() { }
};
class RouterStub {
navigate(url: string) { return url; }
}
TestBed.configureTestingModule({
declarations: [LoginComponent],
imports: [
FormsModule,
HttpModule,
ReactiveFormsModule,
RouterTestingModule
],
providers: [
LoginService,
EmitterService,
AuthTokenService,
{ provide: LoginService, useClass: LoginServiceStub },
// { provide: Router, useClass: RouterStub }
]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(LoginComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('Should log in and navigate to dashboard', fakeAsync(inject([LoginService, Router], (authService: LoginService, router: Router) => {
spyOn(component, 'login');
let button = fixture.debugElement.nativeElement.querySelector('button');
spyOn(authService, 'login').and.returnValue(Observable.of(true));
button.click();
tick();
expect(component.login).toHaveBeenCalled();
expect(component.loading).toBe(false);
})));
});
Problem with this is login function from component is never called When I console.log inside login method in component it display message...
This is Html part:
<form name="form" class="form-horizontal" (ngSubmit)="f.form.valid && login()" #f="ngForm" novalidate>
<img class="loading-img" *ngIf="loading" src="data:image/gif;base64,R0lGODlhEAAQAPIAAP///wAAAMLCwkJCQgAAAGJiYoKCgpKSkiH/C05FVFNDQVBFMi4wAwEAAAAh/hpDcmVhdGVkIHdpdGggYWpheGxvYWQuaW5mbwAh+QQJCgAAACwAAAAAEAAQAAADMwi63P4wyklrE2MIOggZnAdOmGYJRbExwroUmcG2LmDEwnHQLVsYOd2mBzkYDAdKa+dIAAAh+QQJCgAAACwAAAAAEAAQAAADNAi63P5OjCEgG4QMu7DmikRxQlFUYDEZIGBMRVsaqHwctXXf7WEYB4Ag1xjihkMZsiUkKhIAIfkECQoAAAAsAAAAABAAEAAAAzYIujIjK8pByJDMlFYvBoVjHA70GU7xSUJhmKtwHPAKzLO9HMaoKwJZ7Rf8AYPDDzKpZBqfvwQAIfkECQoAAAAsAAAAABAAEAAAAzMIumIlK8oyhpHsnFZfhYumCYUhDAQxRIdhHBGqRoKw0R8DYlJd8z0fMDgsGo/IpHI5TAAAIfkECQoAAAAsAAAAABAAEAAAAzIIunInK0rnZBTwGPNMgQwmdsNgXGJUlIWEuR5oWUIpz8pAEAMe6TwfwyYsGo/IpFKSAAAh+QQJCgAAACwAAAAAEAAQAAADMwi6IMKQORfjdOe82p4wGccc4CEuQradylesojEMBgsUc2G7sDX3lQGBMLAJibufbSlKAAAh+QQJCgAAACwAAAAAEAAQAAADMgi63P7wCRHZnFVdmgHu2nFwlWCI3WGc3TSWhUFGxTAUkGCbtgENBMJAEJsxgMLWzpEAACH5BAkKAAAALAAAAAAQABAAAAMyCLrc/jDKSatlQtScKdceCAjDII7HcQ4EMTCpyrCuUBjCYRgHVtqlAiB1YhiCnlsRkAAAOwAAAAAAAAAAAA==" />
<div class="form-group" [ngClass]="{ 'has-error': f.submitted && !username.valid }">
<label for="username" class="cols-sm-2 control-label">Email</label>
<div class="cols-sm-10">
<div class="input-group">
<span class="input-group-addon"><i class="fa fa-user fa" aria-hidden="true"></i></span>
<input type="text" class="form-control" name="username" placeholder="Your email" [(ngModel)]="model.username" #username="ngModel" required />
</div>
</div>
<div *ngIf="f.submitted && !username.valid" class="help-block">Email is required</div>
</div>
<div class="form-group" [ngClass]="{ 'has-error': f.submitted && !password.valid }">
<label for="password" class="cols-sm-2 control-label">Password</label>
<div class="cols-sm-10">
<div class="input-group">
<span class="input-group-addon"><i class="fa fa-lock fa-lg" aria-hidden="true"></i></span>
<input type="password" placeholder="Your password" class="form-control" name="password" [(ngModel)]="model.password" #password="ngModel" required />
</div>
</div>
<div *ngIf="f.submitted && !password.valid" class="help-block">Password is required</div>
</div>
<div class="form-group">
<button id="login" type="submit" class="btn btn-primary">Login</button>
<div *ngIf="error" style="margin-top: 20px;" class="text-center alert alert-danger">{{error}}</div>
</div>
<div class="form-group text-center login-down" >
<a routerLink="/register" routerLinkActive="active">Register now</a>
<a routerLink="/forgot" routerLinkActive="active">Forgot password</a>
</div>
</form>

You can create mock class for service:
class AuthenticationServiceStub {
login() {}
};
then provide it in configureTestingModule:
TestBed.configureTestingModule({
declarations: [TestComponent],
providers: [
{ provide: AuthenticationService, useClass: AuthenticationServiceStub },
{ provide: Router, useClass: RouterStub }
]
})
inject in your test
inject([AuthenticationService, Router],
(authService: AuthenticationService, router: Router) =>
wrap it in async(+whenStable) or fakeAsync(+tick) or use jasmine.done directly for waiting execution of async methods
it('Should log...', fakeAsync(inject([AuthenticationService, Router]
and mock login method like:
spyOn(authService, 'login').and.returnValue(Observable.of(true) );
Plunker Example
Here is the entire spec:
describe('Welcome component tests', () => {
let comp: TestComponent;
let fixture: ComponentFixture<TestComponent>;
let de: DebugElement;
let el: HTMLElement;
beforeEach(async(() => {
class AuthenticationServiceStub {
login() {}
};
class RouterStub {
navigateByUrl(url: string) { return url; }
}
TestBed.configureTestingModule({
declarations: [TestComponent],
providers: [
{ provide: AuthenticationService, useClass: AuthenticationServiceStub },
{ provide: Router, useClass: RouterStub }
]
})
.compileComponents()
}));
beforeEach(() => {
fixture = TestBed.createComponent(TestComponent);
comp = fixture.componentInstance;
de = fixture.debugElement.query(By.css('.login'));
el = de.nativeElement;
fixture.detectChanges();
});
it('Should log in and navigate to dashboard', fakeAsync(inject([AuthenticationService, Router], (authService: AuthenticationService, router: Router) => {
const spy = spyOn(router, 'navigateByUrl');
spyOn(authService, 'login').and.returnValue(Observable.of(true) );
el.click();
tick();
const navArgs = spy.calls.first().args[0];
expect(navArgs).toBe('/dashboard');
})));
});

Related

Ionic v4 jasmine unit tests with ion-input

I try to test a template driven form in Ionic v4.
But I can`t find a way to get the input element in the ion-input element.
This is what I tried:
login-register.page:
import { GroupsService } from './../../services/groups.service';
import { AuthenticateService, RegisterLoginReturnMessage } from './../../services/authenticate.service';
import { Component, OnInit, OnDestroy, ViewChild } from '#angular/core';
import { ActivatedRoute, Router, NavigationExtras } from '#angular/router';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { AlertController, ToastController } from '#ionic/angular';
import { TranslateService } from '#ngx-translate/core';
import { Location } from '#angular/common';
import { NavController } from '#ionic/angular';
import { addslashes } from './../../helpers';
import { FCM } from '#ionic-native/fcm/ngx';
import { StorageService } from 'src/app/services/storage.service';
import { NgForm } from '#angular/forms';
#Component({
selector: 'app-login-register',
templateUrl: './login-register.page.html',
styleUrls: ['./login-register.page.scss'],
})
export class LoginRegisterPage implements OnInit, OnDestroy {
#ViewChild('loginRegisterForm', { static: true })loginRegisterForm: NgForm;
navParams: any;
email: string;
userName = 'blafasel';
firstName: string;
lastName: string;
password: string;
passwordreset: string;
loading = false;
emailRegExpr = '[a-zA-Z]{1}[a-zA-Z0-9.-_]*#[a-zA-Z0-9]{1}[a-zA-Z0-9.-]*[a-zA-Z0-9]{1}[.][a-zA-Z]{2,}';
gdprChecked = false;
private unsubscribe: Subject<void> = new Subject();
constructor(
private route: ActivatedRoute,
private router: Router,
private authenticateService: AuthenticateService,
public alertController: AlertController,
public translateService: TranslateService,
private location: Location,
private navCtrl: NavController,
private groupsService: GroupsService,
private toastController: ToastController,
private fcm: FCM,
private storageService: StorageService
) {
this.route.queryParams.subscribe(params => {
if (this.router.getCurrentNavigation().extras.state) {
console.log('if (this.router.getCurrentNavigation().extras.state)');
this.navParams = this.router.getCurrentNavigation().extras.state;
console.log(this.navParams);
}
});
}
ngOnInit() {}
ionViewWillEnter() {
console.log('login is entered');
}
submitForm(): void {
this.loading = true;
console.log('submitform');
if (this.navParams.mode === 'register') {
this.authenticateService
.registerUserAndGetMail(this.userName, this.email, this.firstName, this.lastName)
.pipe(takeUntil(this.unsubscribe))
.subscribe(
(response: RegisterLoginReturnMessage) => {
this.loading = false;
if (response.valid === true) {
this.navCtrl.navigateRoot('');
this.presentAlert('success', 'registerSuccess');
} else {
this.presentAlert('error', response.message[0], response.message[1] ? response.message[1] : '');
}
console.log('response:', response);
},
(error: RegisterLoginReturnMessage) => {
this.loading = false;
this.presentAlert('error', error.message[0], error.message[1] ? error.message[1] : '');
console.log('error bla:', error);
}
);
}
if (this.navParams.mode === 'login') {
this.authenticateService
.getUserToken(this.userName, addslashes(this.password))
.then((response: RegisterLoginReturnMessage) => {
this.loading = false;
// this.location.back();
// this.presentAlert('success', response.message[0], response.message[1] ? response.message[1] : '');
this.presentToast('success', response.message[0], response.message[1] ? response.message[1] : '');
// Fetch Groups from backend
this.groupsService.fetchUserData().then(data => {
console.log('promise all data:', data);
this.fcm
.subscribeToTopic(data[2].id)
.then(() => {
console.log('succesfullysubscribe:', data[2].id);
})
.catch(err => {
console.error('error subscription:', err);
});
});
this.navCtrl.navigateRoot('');
console.log('response:', response);
})
.catch((error: RegisterLoginReturnMessage) => {
this.loading = false;
if (error.status === 0) {
this.presentAlert('error', 'wrongConnectionWp');
} else if (error.status === 403) {
this.presentAlert('error', 'wrongLoginData');
} else {
this.presentAlert('error', error.status.toString(), error.statusText);
}
console.log('error bla:', error);
});
}
if (this.navParams.mode === 'passwordReset') {
this.authenticateService
.resetPassword(this.passwordreset)
.then(response => {
this.loading = false;
this.presentAlert('success', response.message);
this.navCtrl.navigateRoot('');
})
.catch(error => {
this.loading = false;
this.presentAlert('error', error.message[0]);
console.log('error in reseet pass catch', error);
});
}
}
presentAlert(header: string, message: string, messageTwo?: string): void {
this.translateService.get([header, message, messageTwo ? messageTwo : '', 'OK']).subscribe(async (res: string[]) => {
const newAlert = await this.alertController.create({
header: res[header],
message: messageTwo ? res[message] + res[messageTwo] : res[message],
buttons: [
{
text: 'OK',
handler: () => {
console.log('ok pressed');
},
},
],
});
await newAlert.present();
});
}
async presentToast(title: string, message: string, message2: string = '', duration?: number): Promise<void> {
this.translateService.get([title, message, message2, 'OK']).subscribe(async (res: string[]) => {
const toast = await this.toastController.create({
header: res[title],
message: message2 ? `${res[message]}<br>${res[message2]}` : `${res[message]}`,
position: 'bottom',
duration: duration ? duration : 3000,
showCloseButton: true,
});
toast.present();
});
}
showPassword(passwordInput) {
passwordInput.type = passwordInput.type === 'password' ? 'text' : 'password';
}
gotToGdpr() {
const bla = this.translateService.currentLang;
console.log('gotogdpr clkicked:', this.translateService.currentLang);
const navigationExtras: NavigationExtras = {
state: {
postId:
this.translateService.currentLang === 'de' ||
this.translateService.currentLang === 'de-AT' ||
this.translateService.currentLang === 'de-CH' ||
this.translateService.currentLang === 'de-DE' ||
this.translateService.currentLang === 'de-LI'
? this.storageService.appData.gdprId.de
: this.storageService.appData.gdprId.en,
},
};
this.router.navigateByUrl('posts', navigationExtras);
}
ngOnDestroy(): void {
// For unsubscribing all Subscriptions
console.log('ngOnDestory');
this.unsubscribe.next();
this.unsubscribe.complete();
}
}
login-register.page.html:
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-back-button text="{{ 'back' | translate }}"></ion-back-button>
</ion-buttons>
<ion-title *ngIf="navParams.mode === 'login'">{{ 'login' | translate }}</ion-title>
<ion-title *ngIf="navParams.mode === 'register'">{{ 'register' | translate }}</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<form #loginRegisterForm="ngForm" (ngSubmit)="submitForm()">
<ion-list inset>
<ion-item lines="inset" *ngIf="navParams.mode === 'register'">
<ion-input
[placeholder]="'email' | translate"
name="email"
id="emailField"
type="text"
required
[(ngModel)]="email"
[pattern]="emailRegExpr"
></ion-input>
</ion-item>
<ion-item lines="inset" *ngIf="navParams.mode === 'register'">
<ion-input
[placeholder]="'username' | translate"
name="userName"
id="userNameField"
type="text"
required
[(ngModel)]="userName"
pattern="[a-zA-Z0-9]{4,30}"
></ion-input>
</ion-item>
<div class="username-message" item-content *ngIf="navParams.mode === 'register'">
{{ 'usernameRestrictions' | translate }}
</div>
<ion-item lines="inset" *ngIf="navParams.mode === 'register'">
<ion-input
[placeholder]="'firstName' | translate"
name="firstName"
id="firstNameField"
type="text"
required
[(ngModel)]="firstName"
pattern="[a-zA-Z0-9\-\s]{1,100}"
></ion-input>
</ion-item>
<ion-item lines="inset" *ngIf="navParams.mode === 'register'">
<ion-input
[placeholder]="'lastName' | translate"
name="lastName"
id="userNameField"
type="text"
required
[(ngModel)]="lastName"
pattern="[a-zA-Z0-9\-\s]{1,100}"
></ion-input>
</ion-item>
<ion-item lines="inset" *ngIf="navParams.mode === 'login'">
<ion-input
[placeholder]="'username' | translate"
name="userName"
id="userNameField"
type="text"
required
[(ngModel)]="userName"
></ion-input>
</ion-item>
<ion-item lines="inset" *ngIf="navParams.mode === 'login'">
<ion-input
#passwordInput
[placeholder]="'password' | translate"
name="password"
id="passwordField"
type="password"
required
[(ngModel)]="password"
></ion-input>
<ion-icon
*ngIf="passwordInput.type === 'password'"
slot="end"
name="eye"
(click)="showPassword(passwordInput)"
style="font-size: 1.7rem;z-index:10"
></ion-icon>
<ion-icon
*ngIf="passwordInput.type == 'text'"
slot="end"
name="eye-off"
(click)="showPassword(passwordInput)"
style="font-size: 1.7rem;z-index:10"
></ion-icon>
</ion-item>
<ion-item lines="inset" *ngIf="navParams.mode === 'passwordReset'">
<ion-input name="passwordreset" id="passwordreset" type="text" required [(ngModel)]="passwordreset"></ion-input>
</ion-item>
<div class="username-message" item-content *ngIf="navParams.mode === 'passwordReset'">
{{ 'passwordReset' | translate }}
</div>
<ion-item lines="inset" *ngIf="navParams.mode === 'register'">
<a (click)="gotToGdpr()">{{ 'privacyPolicyLink' | translate }}</a>
</ion-item>
<ion-item lines="inset" *ngIf="navParams.mode === 'register'">
{{ 'gdprHint' | translate }}
<ion-checkbox pattern="true" name="gdprChecked" slot="end" [(ngModel)]="gdprChecked"></ion-checkbox>
</ion-item>
</ion-list>
<ion-row>
<ion-col>
<ion-button size="full" color="tertiary" type="submit" [disabled]="!loginRegisterForm.form.valid">{{
'submit' | translate
}}</ion-button>
</ion-col>
</ion-row>
</form>
<ion-spinner *ngIf="loading" class="spinner-large spinner-center" color="secondary"></ion-spinner>
</ion-content>
login-register.page.spec.ts:
import { AuthenticateService } from './../../services/authenticate.service';
import { IonicStorageModule } from '#ionic/storage';
import { CUSTOM_ELEMENTS_SCHEMA } from '#angular/core';
import { async, ComponentFixture, TestBed, inject } from '#angular/core/testing';
import { LoginRegisterPage } from './login-register.page';
import { TranslateModule, TranslateLoader } from '#ngx-translate/core';
import { HttpClientModule, HttpClient } from '#angular/common/http';
import { createTranslateLoader } from 'src/app/app.module';
import { FormsModule } from '#angular/forms';
import { IonicModule } from '#ionic/angular';
import { RouterTestingModule } from '#angular/router/testing';
import { FCM } from '#ionic-native/fcm/ngx';
import { FCMMock } from 'src/mocks/fcmMock';
import { Router } from '#angular/router';
import { By } from '#angular/platform-browser';
// test environment
const testModuleConfig = () => {
TestBed.configureTestingModule({
declarations: [LoginRegisterPage],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
providers: [{ provide: FCM, useClass: FCMMock }, AuthenticateService],
imports: [
FormsModule,
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useFactory: createTranslateLoader,
deps: [HttpClient],
},
}),
RouterTestingModule.withRoutes([]),
IonicModule.forRoot(),
IonicStorageModule.forRoot(),
HttpClientModule,
],
}).compileComponents();
};
describe('LoginRegisterPage', () => {
let component: LoginRegisterPage;
let fixture: ComponentFixture<LoginRegisterPage>;
let router: jasmine.SpyObj<Router>;
let service: AuthenticateService;
beforeEach(async(() => {
testModuleConfig();
}));
beforeEach(inject([AuthenticateService], (s) => {
service = s;
router = TestBed.get(Router);
spyOn(router, 'getCurrentNavigation').and.returnValue({ extras: { state: { mode: 'login' } } });
fixture = TestBed.createComponent(LoginRegisterPage);
component = fixture.componentInstance;
fixture.detectChanges();
}));
afterEach(() => {
fixture.destroy();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should be navparams login', () => {
expect(component.navParams).toEqual({ mode: 'login' });
});
it('form invalid when empty', () => {
console.log('component.loginRegisterForm.form', component.loginRegisterForm.form);
expect(component.loginRegisterForm.form.valid).toBeFalsy();
});
it('should contain the correct title', () => {
// component.title = 'Test';
fixture.detectChanges();
// const element = fixture.debugElement.nativeElement.querySelector('#userNameField');
const element = fixture.debugElement.query(By.css('#userNameField input'));
console.log('element+++++++++++++++++', element);
console.log('element+++++++++.textContent++++++++', element.textContent);
const inputs = fixture.debugElement.queryAll(By.css('input'));
console.log('first input element+++++++++++++++++', inputs[0]);
// expect(element.i).toEqual('blafasel');
});
it('test function should return bla', () => {
expect(service.testTestFunction()).toBe('bla');
});
});
The
console.log('element+++++++++++++++++', element);
gives null. But the ion-input element is there with
fixture.debugElement.query(By.css('#userNameField'));
Can somebody help me here to make the input element available in the tests?
Thanx a lot.
You may use the #ViewChild angular decorator to get a reference to the ionic input element into a class instance variable.
For example, in your sample code:
<ion-input
#emailField
[placeholder]="'email' | translate"
name="email"
id="emailField"
type="text"
required
[(ngModel)]="email"
[pattern]="emailRegExpr"></ion-input>
In LoginRegisterPage, you can declare a member as:
#ViewChild('emailField', {static: false}) emailField: IonInput;
You can then access the control's angular component methods directly from the emailField member variable.
HTH.

Regex Validation in template driven form in Angular

I need to validate phone number field in form using regex in template driven form in angular. This is what i have tried
addcontact.component.html
<div class="form-group">
<label for="phone">Phone Number</label>
<input type="text" class="form-control" id="phone" name="phone" required [(ngModel)]="contact.phone"
#phone="ngModel" appPhonevalidator />
<div *ngIf="phone.invalid && (phone.dirty || phone.touched)" class="alert alert-danger">
<div *ngIf="phone.errors.required">
Phone number is required.
</div>
<div *ngIf="phone.errors?.phoneNumberValid">
Phone number is not valid.
</div>
</div>
</div>
phonevalidator.directive.ts
import { Directive } from '#angular/core';
import { Validator, AbstractControl } from '#angular/forms';
import { ValidatorService } from '../_services/validator.service';
import { Observable } from 'rxjs';
#Directive({
selector: '[appPhonevalidator]'
})
export class PhonevalidatorDirective implements Validator {
constructor(private validateService: ValidatorService) {}
validate(control: AbstractControl): Promise<{ [key: string]: any }> | Observable<{ [key: string]: any }> {
console.log(control.value)
return this.validateService.validatePhoneNumber(control.value)
}
}
validator.service.ts
import { Injectable } from '#angular/core';
#Injectable({
providedIn: 'root'
})
export class ValidatorService {
constructor() { }
validatePhoneNumber(phone) {
return new Promise((resolve, reject) => {
setTimeout(() => {
const pattern = new RegExp('^[+]*[(]{0,1}[0-9]{1,4}[)]{0,1}[()-\s\./0-9]*$');
if (pattern.test(phone)) {
resolve({ phoneNumberValid: true })
} else {
resolve({ phoneNumberValid: false })
}
}, 1000);
})
}
}
Its not even going to the directive as expected. Whats wrong in here? I added the directive in declaration and providers in app.module.ts(entry module) (Only giving relevant code here)
app.module.ts
#NgModule({
declarations: [
PhonevalidatorDirective
],
providers: [{
provide: NG_VALIDATORS,
useClass: PhonevalidatorDirective,
multi: true
}],
bootstrap: [AppComponent]
})
export class AppModule { }
UPDATE
After giving the providers in directive it worked, don't know the reason though
#Directive({
selector: '[appPhonevalidator]',
providers: [{ provide: NG_VALIDATORS, useExisting: forwardRef(() => PhonevalidatorDirective), multi: true }]
})

Angular2 trigger button click in unit test and verify event handling

I'm trying to test if click event is properly handled. I'm trying to trigger click event on a button and check if proper call on router was executed.
Don't know why stubRouteSpy did not registered call to navigate (last expect fails)
Template code:
<div class="col-lg-3 col-md-4 col-xs-6 thumb project" *ngFor="let project of projects">
<div class = "thumbnail">
<img class="img-responsive img-rounded" src="{{project.imgUrl}}" alt="">
<div class = "caption">
<h4 id='titleHeader'>{{project.title}}</h4>
<div class="btn-group" role="group" style="all">
<button class="btn btn-primary">Order</button>
<button id='customizeButton' class="btn btn-info" (click)="onCustomize(project.id)">Customize</button>
</div>
</div>
</div>
Component code:
public errorMessage = '';
public projects = [];
constructor(private router: Router, private projectListService: ProjectListService) {
}
public ngOnInit() {
this.getProjects();
}
public getProjects() {
this.projectListService.getAllProjects()
.subscribe(
(projects) => this.projects = projects,
(error) => this.errorMessage = <any> error);
}
public onCustomize(id: string) {
console.log(id);
let navigate = this.router.navigate(['design', id]);
}
Spec code:
describe('GalleryComponent (inline template)', () => {
let comp: GalleryComponent;
let fixture: ComponentFixture<GalleryComponent>;
let projectListService: ProjectListService;
let spy: jasmine.Spy;
let de: DebugElement[];
let stubRoute: Router;
let stubRouteSpy: jasmine.Spy;
beforeEach(() => {
stubRoute = <Router> { navigate: ([]) => {}};
TestBed.configureTestingModule({
declarations: [GalleryComponent],
providers: [
ProjectListService,
{provide: Router, useValue: stubRoute},
{provide: Http, useValue: {}}
],
});
fixture = TestBed.createComponent(GalleryComponent);
comp = fixture.componentInstance;
// ProjectListService actually injected into the component
projectListService = fixture.debugElement.injector.get(ProjectListService);
// Setup spy on the `getAllProjects` method
let fakeProjects = [new Project(1, 'title1', ''), new Project(2, 'title2', '')];
spy = spyOn(projectListService, 'getAllProjects')
.and.returnValue(Observable.of<Project[]>(fakeProjects));
stubRouteSpy = spyOn(stubRoute, 'navigate');
});
it('should navigate to designer when customize button clicked', async(() => {
fixture.detectChanges(); // init
fixture.whenStable().then(() => { // wait for async getAllProjects
fixture.detectChanges(); // update view with projects
fixture.nativeElement.querySelectorAll('#customizeButton')[0].click();
expect(fixture.nativeElement.querySelectorAll('#customizeButton').length).toBe(2); // this pass
fixture.detectChanges();
expect(stubRouteSpy.calls.any()).toBe(true, 'navigate called'); // this is false
});
}));
});
According the documentation here, you should get the provided RouterStub from the injector.
I suggest you use the example in the documentation.
the stub class:
class RouterStub {
navigateByUrl(url: string) { return url; }
}
provide:
{ provide: Router, useClass: RouterStub }
get injector:
routerStub = fixture.debugElement.injector.get(RouterStub);

Unable to test a component with a service

After reading this guide I've decided to test my simple login page which contains just 2 input boxes and a submit button. The component then uses a LoginService to pass these data to backend.
( Also note that I'm new to Unit testing as such, so I'm not sure if this is a good approach as how to test such component. )
For starters, I only wanted to check, if the initial value of #username input element is empty. But I couldn't even make the spec to work due to the below reported issues:
Chrome 55.0.2883 (Windows 7 0.0.0) LoginComponent Username field should be empty FAILED
Failed: Unexpected value 'Http' imported by the module 'DynamicTestModule'
Error: Unexpected value 'Http' imported by the module 'DynamicTestModule'
TypeError: Cannot read property 'detectChanges' of undefined
Chrome 55.0.2883 (Windows 7 0.0.0): Executed 4 of 4 (1 FAILED) (0 secs / 0.348 secs)
When I tried deleting the Http module, I got this error :
Chrome 55.0.2883 (Windows 7 0.0.0) LoginComponent Username field should be empty FAILED
Error: DI Error
Error: Uncaught (in promise): Error: No provider for Http!
TypeError: Cannot read property 'detectChanges' of undefined
Chrome 55.0.2883 (Windows 7 0.0.0): Executed 4 of 4 (1 FAILED) (0 secs / 0.456 secs)
login.component.html
<div class="login jumbotron center-block">
<h1>Login</h1>
<form (ngSubmit)="onSubmit($event)" #loginForm="ngForm">
<div class="form-group">
<label for="username">Username</label>
<input type="text" class="form-control" [(ngModel)]="model.username" name="username"
placeholder="Username" #username="ngModel" required>
<div [hidden]="username.valid || username.pristine" class="alert alert-danger"> Username is required </div>
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" class="form-control" [(ngModel)]="model.password" name="password" placeholder="Password" #password="ngModel" required>
<div [hidden]="password.valid || password.pristine" class="alert alert-danger"> Password is required </div>
</div>
<button type="submit" class="btn btn-default" [disabled]="!loginForm.form.valid" >Submit</button>
<a [routerLink]="['/signup']">Click here to Signup</a>
</form>
</div>
login.component.ts
import { Component } from '#angular/core';
import { Router } from '#angular/router';
import { LoginService } from '../services/login.service';
import { User } from '../extensions/user.class';
#Component({
moduleId: module.id,
selector: 'login',
templateUrl: '../templates/login.component.html',
styleUrls: [ '../styles/login.component.css' ],
providers: [ LoginService ]
})
export class LoginComponent {
private submitted = false;
private model = new User();
constructor(
private router: Router,
private loginService: LoginService
) {}
public onSubmit(event: any): void {
event.preventDefault();
if ( ! this.submitted ) {
this.submitted = true;
if ( this.model.username && this.model.password ) {
this.loginService.login(this.model).then( (token) => {
localStorage.setItem('id_token', token.id);
this.router.navigate(['home']);
}).catch( (error) => this.onLoginFailed(error) );
} else {
console.warn('No username or password provided');
}
}
}
private onLoginFailed( error: any ): void {
//// errors are already handled in login-service ////
console.error(error);
this.submitted = false; /// reset form submit funcitonality ///
}
public signup(event: any): void {
event.preventDefault();
this.router.navigate(['signup']);
}
}
login.component.spec.ts
import { async } from '#angular/core/testing';
import { FormsModule } from '#angular/forms';
import { RouterTestingModule } from '#angular/router/testing';
import { Component } from '#angular/core';
import { Location } from '#angular/common';
import { LoginComponent } from './login.component';
import { LoginService } from '../services/login.service';
import { Http } from '#angular/http';
import { User } from '../extensions/user.class';
#Component({
template: ''
})
class DummyComponent{}
class LoginServiceStub {
login( user: User ){
return true;
}
}
describe('LoginComponent', () => {
let comp: LoginComponent;
let fixture: ComponentFixture<LoginComponent>;
let de: DebugElement;
let el: HTMLElement;
let location: Location;
// async beforeEach
beforeEach( async(() => {
TestBed.configureTestingModule({
declarations: [ LoginComponent, DummyComponent ], // declare the test component
providers: [
{ provide: LoginService, useClass: LoginServiceStub }
],
imports: [
FormsModule ,
RouterTestingModule.withRoutes([
{ path: 'singup', component: DummyComponent }
])
]
}).compileComponents() // compile template and css
.then( () => {
fixture = TestBed.createComponent(LoginComponent);
comp = fixture.componentInstance; // LoginComponent test instance
de = fixture.debugElement.query(By.css('input[name="username"]'));
el = de.nativeElement;
});
}));
it('Username field should be empty', () => {
fixture.detectChanges();
expect(el.textContent).toContain('');
});
});
The problem is that the LoginService is declared at the component level
#Component({
providers: [ LoginService ]
})
This will supersede any same service declared at the module level, which is where you declare the mock in the test. There are a couple things you can do:
Don't declare the service at the component level. If there is not good reason to scope it to the component, then just declare it at the #NgModule.providers and make it a singleton.
Override the #Component.providers in the test.
TestBed.configureTestingModule({})
TestBed.overrideComponent(LoginComponent, {
set: {
providers: [
{ provide: LoginService, useClass: LoginServiceStub }
]
}
});
Alexus,
Have you tried importing the Http module into your test component and adding it to the "providers" array? I think you would have to specify all of your dependencies in this case. I'm assuming your LoginService is requiring {Http} as a provision, but your test component does not register {Http} so it can't find an instance to use.
EDIT:
TestBed.configureTestingModule({
declarations: [ LoginComponent, DummyComponent ], // declare the test component
providers: [
{ provide: LoginService, useClass: LoginServiceStub },
Http,
],
imports: [
FormsModule ,
RouterTestingModule.withRoutes([
{ path: 'singup', component: DummyComponent }
])
]
DOUBLE EDIT!:
In addition, you'll probably want to mock out the Http module, as you won't actually want to send a request during your unit test. "MockBackend" from #angular/http/testing is sufficient for this -- in this case, you would want to use the "provide" syntax you uses with the Login Service to provide an Http module that uses the MockBackend to generate responses.

Angular 2.0.1 A platform with a different configuration has been created. Please destroy it first

I'm trying to run Angular 2 unit tests on an Angular 2 Component with Jasmine (I am not using Karma, however... just webpacking my code then running the tests in the default Jasmine SpecRunner.html).
When I run my code, I get the error: "A platform with a different configuration has been created. Please destroy it first." Been banging my head on this all day. Reading every post on StackOverflow I can find, but I'm still stuck. Any suggestions?
import { ComponentFixture, ComponentFixtureAutoDetect, TestBed, async, fakeAsync, tick } from '#angular/core/testing';
import { By } from '#angular/platform-browser';
import { DebugElement } from '#angular/core';
import {BrowserDynamicTestingModule, platformBrowserDynamicTesting} from "#angular/platform-browser-dynamic/testing";
import {AppLogin} from "../../../app/login/app.login";
describe("Login Component", () => {
let comp: AppLogin;
let fixture: ComponentFixture<AppLogin>;
let el: DebugElement;
function setup() {
TestBed.resetTestEnvironment();
TestBed.initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting());
}
setup();
beforeEach(() => {
TestBed.configureTestingModule({
providers: [AppLogin]
});
fixture = TestBed.createComponent(AppLogin);
comp = fixture.componentInstance;
});
it("login form should pass validation", () => {
fixture.detectChanges();
var form = {
EmailAddress: 'test#me.com',
Password: 'test'
};
var validated = comp.formValidated(form);
expect(validated).toBe(true);
});
});
Here is the component I'm attempting to test...
import { Component } from '#angular/core';
#Component({
selector: 'app-login',
template: `
<form *ngIf="active" (ngSubmit)="onSubmit()" class="form-signin">
<h2 class="form-signin-heading">Please sign in</h2>
<label for="EmailAddress" class="sr-only">Email address</label>
<input type="email" name="EmailAddress" id="EmailAddress" class="form-control" placeholder="Email address"
[(ngModel)]="form.EmailAddress" required autofocus>
<label for="Password" class="sr-only">Password</label>
<input type="password" name="Password" id="Password" class="form-control" placeholder="Password" required
[(ngModel)]="form.Password">
<div class="checkbox">
<label>
<input type="checkbox" id="RememberMe" value="remember-me" [(ngModel)]="form.RememberMe"> Remember me
</label>
</div>
<div *ngIf="form.hasError">
<div *ngFor="let error of form.errorMessages" class="alert alert-danger fade in">{{error.message}}</div>
</div>
<button class="btn btn-lg btn-primary btn-block" type="submit">Sign in</button>
</form>
`
})
export class AppLogin {
form: any;
constructor() {
//
}
formValidated(form: any): boolean {
form.errorMessages = [];
form.hasError = false;
if (form.EmailAddress == null)
form.errorMessages.push({ message: 'Email Address is required.' });
if (form.Password == null)
form.errorMessages.push({ message: 'Password is required.' });
if (form.errorMessages.count > 0)
form.hasError = true;
return !form.hasError;
}
onSubmit(form: any): void {
console.log('Form data: ', form);
}
}
Unfortunately, Jasmine alone did not provide me with the debug information I needed, so I am no longer using Jasmine alone for my unit testing. I am using the recommended Karma/Jasmine setup. (NOTE: However, I am not using the Angular karma-test-shim, which is why I have to run TestBed.initTestEnvironment).
I ran the tests in Karma and I got an error about my component's template. My component template has an angular form. I had to import the angular FormsModule into my test environment. Here is the code which resolved the issue...
import { ComponentFixture, ComponentFixtureAutoDetect, TestBed, async, fakeAsync, tick } from '#angular/core/testing';
import { By, BrowserModule } from '#angular/platform-browser';
import { DebugElement } from '#angular/core';
import {BrowserDynamicTestingModule, platformBrowserDynamicTesting} from "#angular/platform-browser-dynamic/testing";
import { FormsModule } from '#angular/forms';
import {AppLogin} from "../../../app/login/app.login";
describe("Login Component", () => {
let comp: AppLogin;
let fixture: ComponentFixture<AppLogin>;
let el: DebugElement;
beforeEach(() => {
TestBed.resetTestEnvironment();
TestBed.initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting());
TestBed.configureTestingModule({
imports: [ FormsModule, BrowserModule ],
declarations: [ AppLogin ]
});
fixture = TestBed.createComponent(AppLogin);
comp = fixture.componentInstance;
});
it("login form should pass validation", () => {
fixture.detectChanges();
var form = {
EmailAddress: 'test#me.com',
Password: 'test'
};
var validated = comp.formValidated(form);
expect(validated).toBe(true);
});
});
I had a bunch of trouble setting up Karma with Webpack originally, but here is a Karma config I wrote, which is working really well for me (and doesn't require the karma-test-shim)...
module.exports = function(config) {
config.set({
basePath: '',
frameworks: ['jasmine'],
files: [
'src/tests/tests.ts',
'src/tests/login/app.login.spec.ts'
],
exclude: [
],
preprocessors: {
'src/tests/tests.ts': ['webpack'],
'src/tests/login/app.login.spec.ts': ['webpack', 'sourcemap']
},
webpack: {
devtool: 'inline-source-map',
resolve: {
extensions: ['', '.ts', '.js']
},
module: {
loaders: [
{
test: /\.js$/,
loader: 'babel-loader',
query: {
presets: ['es2015']
}
},
{
test: /\.ts$/,
loaders: ['ts-loader']
}
]
}
},
webpackMiddleware: {
// webpack-dev-middleware configuration
noInfo: true
},
plugins: [
require("karma-webpack"),
require("karma-jasmine"),
require("karma-chrome-launcher"),
require("karma-sourcemap-loader"),
require("karma-spec-reporter")
],
reporters: ['spec'],
port: 9876,
colors: true,
// level of logging
// possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
logLevel: config.LOG_INFO,
// enable / disable watching file and executing tests whenever any file changes
autoWatch: true,
// start these browsers
// available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
browsers: ['Chrome'],
// Continuous Integration mode
// if true, Karma captures browsers, runs the tests and exits
singleRun: true
});
};
And finally, here's the code for the tests.ts file I included in my Karma config. This is where I require() all the code I need to run angular tests...
require('zone.js/dist/zone');
require('reflect-metadata');
require('rxjs');
require('#angular/platform-browser');
require('#angular/platform-browser-dynamic');
require('#angular/core');
require('#angular/common');
require('#angular/http');
require('#angular/router');
Error.stackTraceLimit = Infinity;
require('zone.js/dist/long-stack-trace-zone');
require('zone.js/dist/proxy'); // since zone.js 0.6.15
require('zone.js/dist/sync-test');
require('zone.js/dist/jasmine-patch'); // put here since zone.js 0.6.14
require('zone.js/dist/async-test');
require('zone.js/dist/fake-async-test');
var testing = require('#angular/core/testing');