Angular Unit Testing Spy, Router, Fake Async, tick, & Subscribe

Angular Unit Testing of Spy
Angular Unit Testing of Spy
In this blog we are going to cover below all points.
- Unit testing of variables simply defined in component.
- Unit testing of DOM element after set a value using ngModel – To test element value of DOM we use fixture.detectChanges() method to callchange detection life cycle.
- Unit testing of Angular Reactive form. Here I have created form with Observable. So we will show you how can we test subscribe methods.
- Unit testing with spyOn : use of returnValue, callThrough and callFake.
- Unit testing of a service method with jasmine.createSpyObj method.
- Will see here service to be ‘toBeDefined’, callFake with spyObject, returnValue, toHaveBeenCalledWith, toEqual.
- We use callThrough to test actual implementation of service method but with createSpyObj , we can not use callThrough directly. So here we will see how can we use callThrough with createSpyObj.
- Unit test of method that has subscribe call.
- Unit testing of set timeout with fake async and tick
- How to write test case of a function that call HTTP GET request inside it.
- We will see here to writing test case a function that hit a HTTP POST request.
- Unit testing of Router
App Component
import { Component } from "@angular/core"; import { FactoryService } from './services/factory.service'; import { RequestService } from './services/request.service'; import { FormGroup, FormBuilder, Validators } from '@angular/forms'; import { BehaviorSubject } from 'rxjs'; import { Router } from '@angular/router'; @Component({ selector: "app-root", templateUrl: "./app.component.html", styleUrls: ["./app.component.scss"] }) export class AppComponent { /** We will test this public variable: title */ public title = "Hello"; public result = ''; public getResponse; /** Here we have created reactive form with Observable */ private formSource = new BehaviorSubject<FormGroup>(this.getFormFields() ); public form$ = this.formSource.asObservable(); /** Below two (formStatus and userList) are private variables, we will see how can we access private * variables inside spec file */ private formStatus: any; private userList: any; /** * FactoryService - Will show you how to use spyOn with this service methods. * RequestService - We will test this service using jasmine.createSpyObj * Router - We will test router using angular RouterTestingModule */ constructor(private factoryService: FactoryService, private formBuilder: FormBuilder, private requestService: RequestService, private router: Router) { } ngOnInit(): void { } onSubmit() { var result1 = this.factoryService.onSubmit(); console.log("✋ : AppComponent -> onSubmit -> result1", result1); this.result = result1; } getData() { const value = this.requestService.get('this called from component'); this.getResponse = value; console.log("✋ : AppComponent -> getData -> value", value); } callSubscribe() { this.requestService.callSubscribe().subscribe(resp => { console.log("✋ : AppComponent -> getData -> resp", resp); this.getResponse = resp.a; }); } public checkFormValid() { this.form.markAllAsTouched(); if (this.form.invalid) { this.formStatus = 'Form is Invalid'; } else { this.formStatus = 'Form is Valid'; } } private get form() { return this.formSource.value; } private getFormFields() { return this.formBuilder.group({ firstName: ['', Validators.required], email: ['', [Validators.required, Validators.email]] }); } public getUsers() { this.requestService.getUserHttpRequest().subscribe(data => { this.userList = data; }) } public postUser() { const payload = JSON.stringify({ title: 'foo', body: 'bar', userId: 1 }); this.requestService.postUserHttpRequest(payload).subscribe(data => { console.log(data) }) } public asyncTest() { setTimeout(() => { this.userList = [{ name: 'ms' }]; }, 1000); } routeToUser(isParam?: boolean) { if (isParam) { this.router.navigate(['user'], { queryParams: { id: 1, name: 'jsMount' } }); } else { this.router.navigate(['user']); } } }
Factory.service.ts
import { Injectable } from '@angular/core'; import { FormGroup } from '@angular/forms'; import { BehaviorSubject } from 'rxjs'; import { createForm } from '../utilities/unit'; @Injectable({ providedIn: 'root' }) export class FactoryService { private formSource = new BehaviorSubject<FormGroup>(createForm()); form$ = this.formSource.asObservable(); onSubmit() { var calledServiceMethod = true; console.log("✋ : FactoryService -> onSubmit -> calledServiceMethod", calledServiceMethod); return 'actual method called'; } }
Request service with http Implementation
import { HttpClient, HttpHeaders, HttpErrorResponse } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { of, Observable } from 'rxjs'; import { map, catchError, tap } from 'rxjs/operators'; @Injectable({ providedIn: 'root' }) export class RequestService { private getUserURL = 'https://jsonplaceholder.typicode.com/users'; private postUserURL = 'https://jsonplaceholder.typicode.com/posts'; constructor(private http: HttpClient) { } get(value: string) { console.log('actual get method called', value) return value; } callSubscribe() { const value = of({ a: 1234, b: 324234 }); return value; } getUserHttpRequest() { return this.http.get(this.getUserURL); } postUserHttpRequest(payload) { const header = new HttpHeaders(); header.set('Content-type', 'application/json; charset=UTF-8'); return this.http.post(this.postUserURL, payload, { headers: header }) .pipe( tap(response => this.formatResponse(response)), catchError(this.handleError()) ) } // Can use tap operator if U have to transform response in other format else can remove this private formatResponse(response) { const obj = { status: 200, statusText: 'OK', response: response.id } return response['response'] = obj; } private handleError() { return (error: HttpErrorResponse): Observable<any> => { return of(error); }; } }
Let’s create a Spy file of Request Service
export const RequestServiceSpy = jasmine.createSpyObj( 'RequestService', ['get', 'post', 'getUserHttpRequest', 'callSubscribe', 'postUserHttpRequest'] )
Let’s start Unit testing…..
import sections:
import { async, TestBed, tick, fakeAsync } from '@angular/core/testing'; import { FormBuilder } from '@angular/forms'; import { RouterTestingModule } from '@angular/router/testing'; import { of } from 'rxjs'; import { RequestServiceSpy } from 'src/assets/mock/request.service.spy'; import { AppComponent } from './app.component'; import { FactoryService } from './services/factory.service'; import { RequestService } from './services/request.service'; import { HttpClientTestingModule } from '@angular/common/http/testing'; import { CUSTOM_ELEMENTS_SCHEMA, Component } from '@angular/core'; import { Router } from '@angular/router'; /**There are two ways to create a dummy test component to test routing * 1. @Component({ template: '' }) class TestComponent { }; * 2. const TestComponent = class { }; * If define with #1 then we have to add this into declarations as well. * In this example I have used #2. */ const TestComponent = class { }; const routes = [{ path: 'user', component: TestComponent }];
Describe required variables:
describe('AppComponent', () => { let factoryService: FactoryService; let requestServiceSpy: jasmine.SpyObj<RequestService>; let component: AppComponent; let router: Router; }):
Declare all required components, modules and services
//The TestBed is the primary API for writing Angular unit tests beforeEach(async(() => { TestBed.configureTestingModule({ declarations: [ AppComponent ], schemas: [CUSTOM_ELEMENTS_SCHEMA], imports: [ HttpClientTestingModule, RouterTestingModule.withRoutes(routes) ], providers: [FactoryService, { provide: RequestService, useValue: RequestServiceSpy }, FormBuilder,] }).compileComponents(); }));
Define all variables
beforeEach(() => { const fixture = TestBed.createComponent(AppComponent); component = fixture.componentInstance; factoryService = TestBed.get(FactoryService); requestServiceSpy = TestBed.get(RequestService); router = TestBed.get(Router); });
Check Component is defined
it('should create the app', () => { expect(component).toBeDefined(); });
Unit test of a component variable
it(`should have as title 'Hello'`, () => { const fixture = TestBed.createComponent(AppComponent); const app = fixture.componentInstance; expect(app.title).toEqual('Hello'); });
Unit Test of DOM values using fixture.detectChanges()
it('should render title in a h1 tag', () => { const fixture = TestBed.createComponent(AppComponent); // read change detection, if we remove this, this test will be failed because it can not detect changes on dom. fixture.detectChanges(); const compiled = fixture.nativeElement; expect(compiled.querySelector('h1').textContent).toContain("Hello"); });
Unit testing of service with spyOn
it(`should click on submit`, () => { const fixture = TestBed.createComponent(AppComponent); const app = fixture.componentInstance; const submitSpy = spyOn(factoryService, 'onSubmit'); app.onSubmit(); expect(factoryService.onSubmit).toHaveBeenCalled(); // test #2 submitSpy.and.returnValue('Returning value from spy call'); app.onSubmit(); expect(app.result).toEqual('Returning value from spy call'); // test #3 submitSpy.and.callThrough(); app.onSubmit(); expect(app.result).toEqual('actual method called'); // test #4 submitSpy.and.callFake(() => { return 'this is fake call'; }); app.onSubmit(); expect(app.result).toEqual('this is fake call'); });
Unit Testing of service with jasmine.createSpyObj method
it('test requestService with jasmine.createSpyObj method', () => { // test #1 expect(requestServiceSpy).toBeDefined(); // test #2 requestServiceSpy.get.and.callFake(() => { return 'this is call fake'; }); component.getData(); expect(component.getResponse).toEqual('this is call fake'); // test #3 requestServiceSpy.get.and.returnValue('fake'); component.getData(); expect(component.getResponse).toEqual('fake'); // test #4 requestServiceSpy.get('Hello'); expect(requestServiceSpy.get).toHaveBeenCalledWith('Hello'); });
How to Test actual service implementation using callThrough and createSpyObj
it('callThrough not working when creating spy using createSpyObj', () => { // callThrough not working when creating spy using createSpyObj requestServiceSpy.get.and.callThrough(); component.getData(); // expect(component.getResponse).toEqual('this called from component'); > this will not work expect(component.getResponse).toBeUndefined(); });
it('call actual method of requestService using createSpyObj', () => { requestServiceSpy.get.and.callFake(RequestService.prototype.get); component.getData(); expect(component.getResponse).toEqual('this called from component'); });
Unit test of service with jasmine.createSpy
// You can create a object like this that contains all request which are running in component const appServiceStub = { getUserInfo:jasmine.createSpy('getUserInfo').and.returnValue({name: 'js', value: '1201'}), saveUserInfo: jasmine.createSpy('saveUserInfo'), } // Now add same in provider using useValue providers: [ { provide: AppService, useValue: appServiceStub } ] // final write test cases it('Should get User info when called get user', () => { component.getUser(); // called this methd that contain get request method named 'getUserInfo' expect(component.username).toEqual('js'); expect(component.value).toEqual('1201'); });
Unit test of service Error block with jasmine.createSpy
To test the Error block – we have created a spy object inside the unit test function and assign that to the service instance.
it('Should return error when called get user', () => { appService.getUserInfo = jasmine.createSpy('getUserInfo').and.returnValue(throwError({})); component.getUser(); // called this methd that contain get request method named 'getUserInfo' expect(component.username).toEqual(''); expect(component.value).toEqual(''); });
Unit test of subscribe method test
it('test subscribe method', () => { // Test #1 (actual method called) requestServiceSpy.callSubscribe.and.callFake(RequestService.prototype.callSubscribe); component.callSubscribe(); expect(component.getResponse).toEqual(1234); // // Test #2 (call with specific value) requestServiceSpy.callSubscribe.and.returnValue(of({ a: 22222, b: 111111 })); component.callSubscribe(); expect(component.getResponse).toEqual(22222); });
Unit test of set timeout async function with fake async and tick
it('test set timeout with fake async and tick', fakeAsync(() => { component.asyncTest(); tick(1000); expect((component as any).userList.length).toBeGreaterThanOrEqual(1); }));
How to unit test of a function that call a GET HTTP Request
it('Test Get HTTP Request', fakeAsync(() => { const mockUserList = [ { "id": 1, "name": "Leanne Graham", "username": "Bret", "email": "Sincere@april.biz" } ] requestServiceSpy.getUserHttpRequest.and.returnValue(of(mockUserList)); component.getUsers(); expect((component as any).userList.length).toBeGreaterThanOrEqual(1); }));
Unit Testing of POST HTTP Request
it('Test Post HTTP Request', () => { requestServiceSpy.postUserHttpRequest.and.returnValue(of({ id: 1011 })); component.postUser(); expect(requestServiceSpy.postUserHttpRequest).toHaveBeenCalled(); });
Read here how to test a HTTP GET & POST request
https://www.jsmount.com/angular-http-request-testing/
Unit Testing of Routing with RouterTestingModule and Dummy Component
it('Test Routes with RouterTestingModule', () => { const routerSpy = spyOn(router, 'navigate'); // Test #1 without query params component.routeToUser(); expect(routerSpy).toHaveBeenCalledWith([routes[0].path]); // Test #2 with query params component.routeToUser(true); expect(routerSpy).toHaveBeenCalledWith([routes[0].path], { queryParams: { id: 1, name: 'jsMount' } }); });
Unit Testing of Router navigateByUrl method
it('should route to dashboard route when click on Close', fakeAsync(() => { const spy = spyOn(router, 'navigateByUrl'); component.onClose(); const url = spy.calls.first().args[0]; expect(url).toBe('/dashboard'); }));
Unit Testing of EventEmitter emit method
it('should set ChangeFlag when clicking on add property', () => { spyOn(component.changeFlag, 'emit'); component.addProperty(); expect(component.changeFlag.emit).toHaveBeenCalledWith('jsmount'); });
Let’s see how to write unit test of Angular Reactive Form
it('Test Reactive Form is invalid & Learn how to Access private variable inside Spec file', () => { component.checkFormValid(); // Access private variable inside Spec file expect((component as any).formStatus).toEqual('Form is Invalid'); // We have created our form inside subject so can Check with subscribe as well component.form$.subscribe(form => { expect(form.invalid).toEqual(true); // done(); }); });
it('Test Reactive Form is valid', () => { component.checkFormValid(); (component as any).formSource.value.setValue({ firstName: 'JsMount', email: 'JS@gmail.com' }) // We have created our form inside subject so can Check with subscribe as well let form = component.form$.subscribe(form => { expect(form.valid).toEqual(true); }) // Can also unsubscribe our observable. form.unsubscribe(); });
Complete Spec file you can find below for your reference.
import { async, TestBed, tick, fakeAsync } from '@angular/core/testing';
import { FormBuilder } from '@angular/forms';
import { RouterTestingModule } from '@angular/router/testing';
import { of } from 'rxjs';
import { RequestServiceSpy } from 'src/assets/mock/request.service.spy';
import { AppComponent } from './app.component';
import { FactoryService } from './services/factory.service';
import { RequestService } from './services/request.service';
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { CUSTOM_ELEMENTS_SCHEMA, Component } from '@angular/core';
import { Router } from '@angular/router';
/**There are two ways to create a dummy test component to test routing
* 1. @Component({ template: '' }) class TestComponent { };
* 2. const TestComponent = class { };
* If define with #1 then we have to add this into declarations as well.
* In this example I have used #2.
*/
const TestComponent = class { };
const routes = [{
path: 'user',
component: TestComponent
}];
describe('AppComponent', () => {
let factoryService: FactoryService;
let requestServiceSpy: jasmine.SpyObj<RequestService>;
let component: AppComponent;
let router: Router;
//The TestBed is the primary API for writing Angular unit tests
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [
AppComponent
],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
imports: [
HttpClientTestingModule,
RouterTestingModule.withRoutes(routes)
],
providers: [FactoryService,
{ provide: RequestService, useValue: RequestServiceSpy },
FormBuilder,]
}).compileComponents();
}));
beforeEach(() => {
const fixture = TestBed.createComponent(AppComponent);
component = fixture.componentInstance;
factoryService = TestBed.get(FactoryService);
requestServiceSpy = TestBed.get(RequestService);
router = TestBed.get(Router);
});
it('should create the app', () => {
expect(component).toBeDefined();
});
it(`should have as title 'Hello'`, () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
expect(app.title).toEqual('Hello');
});
it('should render title in a h1 tag', () => {
const fixture = TestBed.createComponent(AppComponent);
// read change detection, if we remove this, this test will be failed because it can not detect changes on dom.
fixture.detectChanges();
const compiled = fixture.nativeElement;
expect(compiled.querySelector('h1').textContent).toContain("Hello");
});
it(`should click on submit`, () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
const submitSpy = spyOn(factoryService, 'onSubmit');
app.onSubmit();
expect(factoryService.onSubmit).toHaveBeenCalled();
// test #2
submitSpy.and.returnValue('Returning value from spy call');
app.onSubmit();
expect(app.result).toEqual('Returning value from spy call');
// test #3
submitSpy.and.callThrough();
app.onSubmit();
expect(app.result).toEqual('actual method called');
// test #4
submitSpy.and.callFake(() => {
return 'this is fake call';
});
app.onSubmit();
expect(app.result).toEqual('this is fake call');
});
it('test requestService with jasmine.createSpyObj method', () => {
// test #1
expect(requestServiceSpy).toBeDefined();
// test #2
requestServiceSpy.get.and.callFake(() => {
return 'this is call fake';
});
component.getData();
expect(component.getResponse).toEqual('this is call fake');
// test #3
requestServiceSpy.get.and.returnValue('fake');
component.getData();
expect(component.getResponse).toEqual('fake');
// test #4
requestServiceSpy.get('Hello');
expect(requestServiceSpy.get).toHaveBeenCalledWith('Hello');
});
it('callThrough not working when creating spy using createSpyObj', () => {
// callThrough not working when creating spy using createSpyObj
requestServiceSpy.get.and.callThrough();
component.getData();
// expect(component.getResponse).toEqual('this called from component'); > this will not work
expect(component.getResponse).toBeUndefined();
});
it('call actual method of requestService using createSpyObj', () => {
requestServiceSpy.get.and.callFake(RequestService.prototype.get);
component.getData();
expect(component.getResponse).toEqual('this called from component');
});
it('test subscribe method', () => {
// Test #1 (actual method called)
requestServiceSpy.callSubscribe.and.callFake(RequestService.prototype.callSubscribe);
component.callSubscribe();
expect(component.getResponse).toEqual(1234);
// // Test #2 (call with specific value)
requestServiceSpy.callSubscribe.and.returnValue(of({ a: 22222, b: 111111 }));
component.callSubscribe();
expect(component.getResponse).toEqual(22222);
});
it('Test Reactive Form is invalid & Learn how to Access private variable inside Spec file', () => {
component.checkFormValid();
// Access private variable inside Spec file
expect((component as any).formStatus).toEqual('Form is Invalid');
// We have created our form inside subject so can Check with subscribe as well
component.form$.subscribe(form => {
expect(form.invalid).toEqual(true);
});
});
it('Test Reactive Form is valid', () => {
component.checkFormValid();
(component as any).formSource.value.setValue({ firstName: 'JsMount', email: 'JS@gmail.com' })
// We have created our form inside subject so can Check with subscribe as well
let form = component.form$.subscribe(form => {
expect(form.valid).toEqual(true);
})
// Can also unsubscribe our observable.
form.unsubscribe();
});
it('test set timeout with fake async and tick', fakeAsync(() => {
component.asyncTest();
tick(1000);
expect((component as any).userList.length).toBeGreaterThanOrEqual(1);
}));
xit('Test Get HTTP Request', fakeAsync(() => {
const mockUserList = [
{
"id": 1,
"name": "Leanne Graham",
"username": "Bret",
"email": "Sincere@april.biz"
}
]
requestServiceSpy.getUserHttpRequest.and.returnValue(of(mockUserList));
component.getUsers();
expect((component as any).userList.length).toBeGreaterThanOrEqual(1);
}));
it('Test Post HTTP Request', () => {
requestServiceSpy.postUserHttpRequest.and.returnValue(of({ id: 1011 }));
component.postUser();
expect(requestServiceSpy.postUserHttpRequest).toHaveBeenCalled();
});
it('Test Routes with RouterTestingModule', () => {
const routerSpy = spyOn(router, 'navigate');
// Test #1 without query params
component.routeToUser();
expect(routerSpy).toHaveBeenCalledWith([routes[0].path]);
// Test #2 with query params
component.routeToUser(true);
expect(routerSpy).toHaveBeenCalledWith([routes[0].path], { queryParams: { id: 1, name: 'jsMount' } });
});
});
/**
* IF you are getting syntax errors like expect not found, describe not found etc..
* npm install --save-dev @types/jasmine
*/