angular unit testing

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
 */


https://jasmine.github.io/