I'm trying to start working with Unit Testing in my Ionic application, with Angular 6, Jasmine and Karma.
I have a loginPage, this calls an AuthService which in turn calls an HttpService, below I show you the code.
The problem I'm having is that if I call the doLogin function (in the loginPage) or if I call the login function (in the authService) I can never get the response. I tried using a subscribe and executing toPromise() to convert the Observable into a Promise, but I didn't succeed either.
Can you help me? Thanks a lot.
LOGINPAGE.TS
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Credentials } from '../models/credentials';
import { AuthService } from '../services/auth.service';
@Component({
selector: 'app-login',
templateUrl: './login.page.html',
styleUrls: ['./login.page.scss'],
})
export class LoginPage implements OnInit {
ionicForm: FormGroup;
isSubmitted: boolean = false;
credential: Credentials;
constructor(public formBuilder: FormBuilder, private _authService: AuthService) { }
ngOnInit() {
this.ionicForm = this.formBuilder.group({
email: ['[email protected]', [Validators.required, Validators.minLength(8)]],
password: ['cityslicka', [Validators.required, Validators.minLength(8)]],
});
}
doLogin1() {
this.isSubmitted = true;
if (!this.ionicForm.valid) {
return false;
} else {
return true
}
}
async doLogin(): Promise<Credentials | false> {
this.isSubmitted = true;
if (!this.ionicForm.valid) {
return false;
} else {
this.credential = await this._authService.login(this.ionicForm.value);
return this.credential;
}
}
get errorControl() {
return this.ionicForm.controls;
}
}
AUTHSERVICE.TS
import { Injectable } from '@angular/core';
import { HttpService } from './http.service';
@Injectable({
providedIn: 'root'
})
export class AuthService {
constructor(private _httpService: HttpService) { }
async login(body){
return await this._httpService.post('https://reqres.in/api/login',body).toPromise();
}
}
HTTPSERVICE.TS
import { Injectable } from '@angular/core';
import { Observable, Subject, throwError } from 'rxjs';
import { HttpClient, HttpErrorResponse, HttpHeaders} from '@angular/common/http';
import { catchError } from 'rxjs/operators';
@Injectable({
providedIn: 'root'
})
export class HttpService {
errorLogin = new Subject<boolean>();
constructor(private httpClient: HttpClient) {
}
public get(path: string, options?: any): Observable<any> {
return this.httpClient.get(path,options).pipe(catchError(this.formatErrors));
}
public put(path: string, body: object = {},headers?:any): Observable<any> {
let options = {
headers: headers
}
return this.httpClient.put(path, JSON.stringify(body),options).pipe(catchError(this.formatErrors));
}
public post(path: string, body: object = {},options?: any): Observable<any> {
return this.httpClient.post(path,body,options).pipe(catchError(this.formatErrors));
}
public delete(path: string, headers?:any): Observable<any> {
return this.httpClient.delete(path, {headers,responseType: 'text' }).pipe(catchError(this.formatErrors));
}
public appendHeaders(header:HttpHeaders){
if (header != undefined){
return header.set('Content-Type','application/json');
}
return new HttpHeaders().set('Content-Type','application/json');
}
public formatErrors(error: HttpErrorResponse): Observable<any> {
if (error.status === 401 && error.error.message == 'Authentication failed'){
try {
this.errorLogin.next(true);
} catch(error){
}
}
return throwError(error);
}
showCredError(){
this.errorLogin.next(true);
}
hideCredError(){
this.errorLogin.next(false);
}
/*
Metodo de prueba, eliminar al pasar a producción
*/
public testRest(){
return this.httpClient.get('https://jsonplaceholder.typicode.com/todos/1');
}
}
LOGINPAGE.SPEC.TS
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { FormsModule, ReactiveFormsModule, Validators } from '@angular/forms';
import { By } from '@angular/platform-browser';
import { IonicModule } from '@ionic/angular';
import { HttpClientTestingModule, } from '@angular/common/http/testing';
import { LoginPage } from './login.page';
import { AuthService } from '../services/auth.service';
import { HttpService } from '../services/http.service';
import { of } from 'rxjs';
import { Credentials } from '../models/credentials';
describe('LoginPage', () => {
let component: LoginPage;
let fixture: ComponentFixture<LoginPage>;
let authService: AuthService;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [ LoginPage ],
imports: [IonicModule.forRoot(),
FormsModule,
ReactiveFormsModule,
HttpClientTestingModule],
providers: [AuthService, HttpService]
}).compileComponents();
fixture = TestBed.createComponent(LoginPage);
component = fixture.componentInstance;
fixture.detectChanges();
jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000;
}));
it('A) should create', () => {
expect(component).toBeTruthy();
});
it('B) send form without data', () =>{
component.ionicForm = component.formBuilder.group({
email: ['', [Validators.required, Validators.minLength(8)]],
password: ['', [Validators.required, Validators.minLength(8)]],
});
component.doLogin().then((data)=>{
expect(data).toBeFalse();
});
fixture.detectChanges();
expect(fixture.debugElement.query(By.css('.errorUsernameRequired'))).not.toBeNull();
expect(fixture.debugElement.query(By.css('.errorPasswordRequired'))).not.toBeNull();
});
it('C) send form without password', () =>{
component.ionicForm = component.formBuilder.group({
email: ['UsuarioPrueba', [Validators.required, Validators.minLength(8)]],
password: ['', [Validators.required, Validators.minLength(8)]],
});
component.doLogin().then((data)=>{
expect(data).toBeFalse();
});
fixture.detectChanges();
expect(fixture.debugElement.query(By.css('.errorUsernameRequired'))).toBeNull();
expect(fixture.debugElement.query(By.css('.errorPasswordRequired'))).not.toBeNull();
});
it('D) send form without username', () =>{
component.ionicForm = component.formBuilder.group({
email: ['', [Validators.required, Validators.minLength(8)]],
password: ['ContraseñaPrueba', [Validators.required, Validators.minLength(8)]],
});
component.doLogin().then((data)=>{
expect(data).toBeFalse();
});
fixture.detectChanges();
expect(fixture.debugElement.query(By.css('.errorUsernameRequired'))).not.toBeNull();
expect(fixture.debugElement.query(By.css('.errorPasswordRequired'))).toBeNull();
});
it('E) send form with minor lenght in both inputs', () =>{
component.ionicForm = component.formBuilder.group({
email: ['ABCDEF', [Validators.required, Validators.minLength(8)]],
password: ['ABCDEF', [Validators.required, Validators.minLength(8)]],
});
component.doLogin().then((data)=>{
expect(data).toBeFalse();
});
fixture.detectChanges();
expect(fixture.debugElement.query(By.css('.errorPasswordMinLength'))).not.toBeNull();
expect(fixture.debugElement.query(By.css('.errorUsernameMinLength'))).not.toBeNull();
});
it('F) send form successfully', (()=>{
component.ionicForm = component.formBuilder.group({
email: ['[email protected]', [Validators.required, Validators.minLength(8)]],
password: ['cityslicka', [Validators.required, Validators.minLength(8)]],
});
component.doLogin().then((data)=>{
console.log(data);
// THIS CONSOLE LOG NOT APPEAR IN THE CHROME CONSOLE
})
}));
});
CodePudding user response:
I would drop HttpClientTestingModule
and just mock AuthService
directly.
Follow the lines with // !!:
describe('LoginPage', () => {
let component: LoginPage;
let fixture: ComponentFixture<LoginPage>;
// !! change this line to mockAuthService
let mockAuthService: jasmine.SpyObj<AuthService>;
beforeEach(waitForAsync(() => {
// !! assign mockAuthService to mock here
// !! first string argument is optional, 2nd array of strings are public methods you would like to mock
mockAuthService = jasmine.createSpyObj<AuthService>('AuthService', ['login']);
TestBed.configureTestingModule({
declarations: [ LoginPage ],
imports: [IonicModule.forRoot(),
FormsModule,
ReactiveFormsModule],
// !! provide mock for the real AuthService
providers: [{ provide: AuthService, useValue: mockAuthService }]
}).compileComponents();
fixture = TestBed.createComponent(LoginPage);
component = fixture.componentInstance;
fixture.detectChanges();
jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000;
}));
// !! you have an extra bracket after the ', and I think this could be a problem
it('F) send form successfully', ()=>{
component.ionicForm = component.formBuilder.group({
email: ['[email protected]', [Validators.required, Validators.minLength(8)]],
password: ['cityslicka', [Validators.required, Validators.minLength(8)]],
});
mockAuthService.login.and.returnValue(Promise.resolve({/* mock login respond here */}));
// !! below should hopefully work now
component.doLogin().then((data)=>{
console.log(data);
// THIS CONSOLE LOG NOT APPEAR IN THE CHROME CONSOLE
})
})
The above is a unit test. The mentality for a unit test should be assume everything else works, does my component/class/etc. do what it is supposed to? Read this for mocking external dependencies.
What you're aiming for is an integration test (providing actual AuthService) and I think we can do that too. We have to look at the http request in queue and respond to it.
describe('LoginPage', () => {
let component: LoginPage;
let fixture: ComponentFixture<LoginPage>;
let authService: AuthService;
// !! get a handle on httpTestingController
let httpTestingController: HttpTestingController;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [ LoginPage ],
imports: [IonicModule.forRoot(),
FormsModule,
ReactiveFormsModule,
HttpClientTestingModule],
providers: [AuthService, HttpService]
}).compileComponents();
fixture = TestBed.createComponent(LoginPage);
component = fixture.componentInstance;
// !! get handle, if you're using Angular 9 , get should be inject
httpTestingController = TestBed.get(HttpTestingController);
fixture.detectChanges();
jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000;
}));
// !! remove the ( after ',
it('F) send form successfully', () => {
component.ionicForm = component.formBuilder.group({
email: ['[email protected]', [Validators.required, Validators.minLength(8)]],
password: ['cityslicka', [Validators.required, Validators.minLength(8)]],
});
component.doLogin().then((data)=>{
console.log(data);
// THIS CONSOLE LOG NOT APPEAR IN THE CHROME CONSOLE
});
// !! get a handle on request
const request = httpTestingController.expectOne(request => request.url.includes('login'));
request.flush({/* flush what you would like for data to be logged */});
});