Learn Angular 4.0 In 10 Days - Unit Test - Day Ten (Part Two)

Let's start the final  part of the article series "Learn Angular 4.0 in 10 Days." In the first part, we discussed the below topics related to the Unit Test in Angular Framework:
  1. Concept of Unit Test
  2. Available Frameworks or Tools on Unit Test
  3. Why to Perform Unit Test
  4. Setting Environment of Unit Test
  5. Peform a Unit Test of a Basic Class
  6. Perform a Unit Test of a Parameter Based Method in Class
Now, in this second part, we will discuss:
  1. Unit Test of Angular Component
  2. Unit Test of Angular Directives
  3. Unit Test of Angular Service 
If you want to read the previous articles of this series, do visit the below links.
Unit Test of Angular Components
 
Now, for performing unit tests on an Angular component, we first need to develop an Angular component as below.
  1. import { Component, Input, Output, EventEmitter } from '@angular/core';  
  2.   
  3. @Component({  
  4.     selector: 'student-template-data',  
  5.     template: `  
  6.                 <div class="form-horizontal">  
  7.                 <h2 class="aligncenter">Student Details (Template)</h2><br />  
  8.                 <div class="row">  
  9.                     <div class="col-xs-12 col-sm-2 col-md-2">  
  10.                         <span>First Name</span>  
  11.                     </div>  
  12.                     <div class="col-xs-12 col-sm-4 col-md-4">  
  13.                         <input type="text" id="txtFName" placeholder="Enter First Name" #firstName/>  
  14.                     </div>  
  15.                     <div class="col-xs-12 col-sm-2 col-md-2">  
  16.                         <span>Last Name</span>  
  17.                     </div>  
  18.                     <div class="col-xs-12 col-sm-4 col-md-4">  
  19.                         <input type="text" id="txtLName" placeholder="Enter Second Name" #lastName/>  
  20.                     </div>  
  21.                 </div>  
  22.                 <br />  
  23.                 <div class="row">  
  24.                       
  25.                     <div class="col-xs-12 col-sm-2 col-md-2">  
  26.                         <span>Email</span>  
  27.                     </div>  
  28.                     <div class="col-xs-12 col-sm-4 col-md-4">  
  29.                         <input type="email" id="txtEmail" #email/>  
  30.                     </div>  
  31.                     <div class="col-xs-12 col-sm-2 col-md-2">  
  32.                     </div>  
  33.                     <div class="col-xs-12 col-sm-4 col-md-4">  
  34.                         <input type="button" id="btnSubmit" value="Submit" class="btn btn-primary" [disabled]="!enabled"   
  35.                                 (click)="onSubmit(firstName.value,lastName.value,email.value)"/>  
  36.                     </div>  
  37.                 </div>  
  38.             </div>  
  39.               `  
  40. })  
  41.   
  42. export class StudentTemplateComponent {  
  43.   
  44.     private _model: any = {};  
  45.   
  46.     @Input() enabled:boolean = true;  
  47.     @Output() onFormSubmit: EventEmitter<any> = new EventEmitter<any>();  
  48.   
  49.     constructor() {  
  50.     }  
  51.   
  52.     private onSubmit(firstName:string, lastName:string, email : string): void {  
  53.         this._model.firstName = firstName;  
  54.         this._model.lastName = lastName;  
  55.         this._model.email = email;  
  56.         if (typeof (this._model) === "undefined") {  
  57.             alert("Form not Filled Up Properly");  
  58.         }  
  59.         else {  
  60.             alert("Data Is Correct");  
  61.             this.onFormSubmit.emit(this._model);  
  62.         }  
  63.     }  
  64.   
  65.     private onClear(): void {  
  66.         this._model = {};  
  67.     }  
  68. }  
So, in the above component, we have used the following features.
  • Input Properties of a Component
  • Output Properties of a Component for Button Event Emit
  • Also, we used inline HTML template using Template Selector in the @Component Decorator.
Testing @Input() Decorator
 
In fact, in Angular 4, every input property is just like a simple variable object value which can be set from outside of the component using selector or using component instance as an object. So normally, we can set the value of input properties as below in our unit test specs.
  1. it('Setting value to the input properties variables", () => {  
  2.   component.enabled = false;  
  3. });  
If we want to validate the value of the input properties in the components, then we can write the specs code as below.
  1. it('Setting value to input properties on button click', () => {  
  2.     component.enabled = false;  
  3.     fixture.detectChanges();  
  4.     expect(submitEl.nativeElement.disabled).toBeTruthy();  
  5. });  
In both of the above blocks, we used fixture.detectChanges() for firing the change detection event and updating the View as per the value.
 
Testing @Output() Decorator
 
Tesing an Output event is not as simple as an Input method. Because output event is actually an observable object so that we can subscribe to it and get a callback for every event. So for the Output event, we need to raise the event of the specified control in the View. Also, in the output event, we may need to assign values in the input controls of the forms. 
 
Unit Test code of the above mentioned components
  1. import {TestBed, ComponentFixture, inject, async} from '@angular/core/testing';  
  2. import { StudentTemplateComponent } from '../component/app.component.template';  
  3. import {Component, DebugElement} from "@angular/core";  
  4. import {By} from "@angular/platform-browser";  
  5.   
  6.   
  7. describe('Component: Student Form ', () => {  
  8.   
  9.   let component: StudentTemplateComponent;  
  10.   let fixture: ComponentFixture<StudentTemplateComponent>;  
  11.   let submitEl: DebugElement;  
  12.   let firstNameEl: DebugElement;  
  13.   let lastNameEl: DebugElement;  
  14.   let emailEl : DebugElement;  
  15.   
  16.   beforeEach(async(() => {  
  17.     TestBed.configureTestingModule({  
  18.       declarations: [ StudentTemplateComponent ]  
  19.     });  
  20.   
  21.     fixture = TestBed.createComponent(StudentTemplateComponent);  
  22.     component = fixture.componentInstance;  
  23.   
  24.     submitEl = fixture.debugElement.query(By.css('input[id=btnSubmit]'));  
  25.     firstNameEl = fixture.debugElement.query(By.css('input[id=txtFName]'));  
  26.     lastNameEl = fixture.debugElement.query(By.css('input[id=txtLName]'));  
  27.     emailEl = fixture.debugElement.query(By.css('input[type=email]'));  
  28.   }));  
  29.   
  30.   it('Setting value to input properties on form load', () => {  
  31.     component.enabled = false;  
  32.     fixture.detectChanges();  
  33.     expect(submitEl.nativeElement.disabled).toBeTruthy();  
  34.   });  
  35.   
  36.   it('Setting value to input properties on button click', () => {  
  37.     component.enabled = true;  
  38.     fixture.detectChanges();  
  39.     expect(submitEl.nativeElement.disabled).toBeFalsy();  
  40.   });  
  41.   
  42.   it('Entering value in input controls and emit output events', () => {  
  43.     let user: any;  
  44.     firstNameEl.nativeElement.value = "Debasis";  
  45.     lastNameEl.nativeElement.value = "Saha";  
  46.     emailEl.nativeElement.value = "debasis@yahoo.com";  
  47.   
  48.     // Subscribe to the Observable and store the user in a local variable.  
  49.     component.onFormSubmit.subscribe((value) => user = value);  
  50.   
  51.     // This sync emits the event and the subscribe callback gets executed above  
  52.     submitEl.triggerEventHandler('click'null);  
  53.   
  54.     // Now we can check to make sure the emitted value is correct  
  55.     expect(user.firstName).toBe("Debasis");  
  56.     expect(user.lastName).toBe("Saha");  
  57.     expect(user.email).toBe("debasis@yahoo.com");  
  58.   });  
  59. });  
The output is:

 
Now, in the above component, we use Template selector for defining the UI related HTML Code. But if we want to use TemplateUrl in the componet so that we can decouple the HTML View with our component, in that scenerio we need to make some changes in our unit test code. In the above example, we created the instance of the component using the below code.
  1. TestBed.configureTestingModule({  
  2.       declarations: [ StudentTemplateComponent ]  
  3.     });  
  4.   
  5.     fixture = TestBed.createComponent(StudentTemplateComponent);  
  6.     component = fixture.componentInstance;  
But if we we use TemplateUrl, then we need to change this code as below because at that time, we need to call compile() for retrieving the UI components from the browsers.
  1. TestBed.configureTestingModule({  
  2.       declarations: [ StudentDataComponent ],  
  3.     })  
  4.     .compileComponents()  
  5.     .then(() => {  
  6.         fixture = TestBed.createComponent(StudentDataComponent);  
  7.         component = fixture.componentInstance;  
  8.         
  9.     });  
Sample Code for app.component.student.html
  1. <div class="form-horizontal">  
  2.     <h2 class="aligncenter">Student Details (Template Url)</h2><br />  
  3.     <div class="row">  
  4.         <div class="col-xs-12 col-sm-2 col-md-2">  
  5.             <span>First Name</span>  
  6.         </div>  
  7.         <div class="col-xs-12 col-sm-4 col-md-4">  
  8.             <input type="text" id="txtFName" placeholder="Enter First Name" #firstName/>  
  9.         </div>  
  10.         <div class="col-xs-12 col-sm-2 col-md-2">  
  11.             <span>Last Name</span>  
  12.         </div>  
  13.         <div class="col-xs-12 col-sm-4 col-md-4">  
  14.             <input type="text" id="txtLName" placeholder="Enter Second Name" #lastName/>  
  15.         </div>  
  16.     </div>  
  17.     <br />  
  18.     <div class="row">  
  19.           
  20.         <div class="col-xs-12 col-sm-2 col-md-2">  
  21.             <span>Email</span>  
  22.         </div>  
  23.         <div class="col-xs-12 col-sm-4 col-md-4">  
  24.             <input type="email" id="txtEmail" #email/>  
  25.         </div>  
  26.         <div class="col-xs-12 col-sm-2 col-md-2">  
  27.         </div>  
  28.         <div class="col-xs-12 col-sm-4 col-md-4">  
  29.             <input type="button" id="btnSubmit" value="Submit" class="btn btn-primary" [disabled]="!enabled"   
  30.                     (click)="onSubmit(firstName.value,lastName.value,email.value)"/>  
  31.         </div>  
  32.     </div>  
  33. </div>  
Sample code for app.component.student.ts
  1. import { Component, Input, Output, EventEmitter } from '@angular/core';  
  2.   
  3. @Component({  
  4.     // moduleId: module.id,  
  5.     selector: 'student-data',  
  6.     templateUrl: 'app/component/app.component.student.html'  
  7. })  
  8.   
  9. export class StudentDataComponent {  
  10.   
  11.     private _model: any = {};  
  12.   
  13.     @Input() enabled:boolean = true;  
  14.     @Output() onFormSubmit: EventEmitter<any> = new EventEmitter<any>();  
  15.   
  16.     constructor() {  
  17.     }  
  18.   
  19.     private onSubmit(firstName:string, lastName:string, email : string): void {  
  20.         this._model.firstName = firstName;  
  21.         this._model.lastName = lastName;  
  22.         this._model.email = email;  
  23.         if (typeof (this._model) === "undefined") {  
  24.             alert("Form not Filled Up Properly");  
  25.         }  
  26.         else {  
  27.             alert("Data Is Correct");  
  28.             this.onFormSubmit.emit(this._model);  
  29.         }  
  30.     }  
  31.   
  32.     private onClear(): void {  
  33.         this._model = {};  
  34.     }  
  35. }  
Sample code for app.component.student.spec.ts 
  1. import {TestBed, ComponentFixture, inject, async} from '@angular/core/testing';  
  2. import { StudentDataComponent } from '../component/app.component.student';  
  3. import {Component, DebugElement} from "@angular/core";  
  4. import {By} from "@angular/platform-browser";  
  5.   
  6.   
  7. describe('Component: Student Form ', () => {  
  8.   
  9.   let component: StudentDataComponent;  
  10.   let fixture: ComponentFixture<StudentDataComponent>;  
  11.   let submitEl: DebugElement;  
  12.   let firstNameEl: DebugElement;  
  13.   let lastNameEl: DebugElement;  
  14.   let emailEl : DebugElement;  
  15.   
  16.   beforeEach(async(() => {  
  17.     TestBed.configureTestingModule({  
  18.       declarations: [ StudentDataComponent ],  
  19.     })  
  20.     .compileComponents()  
  21.     .then(() => {  
  22.         fixture = TestBed.createComponent(StudentDataComponent);  
  23.         component = fixture.componentInstance;  
  24.         submitEl = fixture.debugElement.query(By.css('input[id=btnSubmit]'));  
  25.         firstNameEl = fixture.debugElement.query(By.css('input[id=txtFName]'));  
  26.         lastNameEl = fixture.debugElement.query(By.css('input[id=txtLName]'));  
  27.         emailEl = fixture.debugElement.query(By.css('input[type=email]'));  
  28.     });  
  29.   }));  
  30.   
  31.   it('Setting enabled to false disabled the submit button', () => {  
  32.     component.enabled = false;  
  33.     fixture.detectChanges();  
  34.     expect(submitEl.nativeElement.disabled).toBeTruthy();  
  35.   });  
  36.   
  37.   it('Setting enabled to true enables the submit button', () => {  
  38.     component.enabled = true;  
  39.     fixture.detectChanges();  
  40.     expect(submitEl.nativeElement.disabled).toBeFalsy();  
  41.   });  
  42.   
  43.   it('Entering value in input controls and emit output events', () => {  
  44.     let user: any;  
  45.     firstNameEl.nativeElement.value = "Debasis";  
  46.     lastNameEl.nativeElement.value = "Saha";  
  47.     emailEl.nativeElement.value = "debasis@yahoo.com";  
  48.   
  49.     // Subscribe to the Observable and store the user in a local variable.  
  50.     component.onFormSubmit.subscribe((value) => user = value);  
  51.   
  52.     // This sync emits the event and the subscribe callback gets executed above  
  53.     submitEl.triggerEventHandler('click'null);  
  54.   
  55.     // Now we can check to make sure the emitted value is correct  
  56.     expect(user.firstName).toBe("Debasis");  
  57.     expect(user.lastName).toBe("Saha");  
  58.     expect(user.email).toBe("debasis@yahoo.com");  
  59.   });  
  60. });  
 Unit Test of an Angular Attribute Directive
 
Now we are going to perform unit test on an attribute directive called FocusDirectives. If these directives are used in any html element, then on mouse over, the color of that element is changed to Red. In the earlier articles, we already discussed that in directives, we need to use @HostListenner to capture the the mouse related events of the input elements and also we need to use @HostBinding to set the style property of the input elements. So for tracking the mouse events, we need to use triggerEventHandler to stimulate events. 
 
Sample code of directives 
  1. import {  
  2.     Directive,  
  3.     HostListener,  
  4.     HostBinding  
  5. } from '@angular/core';  
  6.   
  7. @Directive({  
  8.   selector: '[onFocus]'  
  9. })  
  10. export class FocusDirective {  
  11.   
  12.   @HostBinding("style.background-color") backgroundColor: string;  
  13.   
  14.   @HostListener('mouseover') onHover() {  
  15.     this.backgroundColor = 'red';  
  16.   }  
  17.   
  18.   @HostListener('mouseout') onLeave() {  
  19.     this.backgroundColor = 'inherit';  
  20.   }  
  21. }  
Sample code of the unit test specs of the directives
  1. import {TestBed, ComponentFixture} from '@angular/core/testing';  
  2. import {Component, DebugElement} from "@angular/core";  
  3. import {By} from "@angular/platform-browser";  
  4. import {FocusDirective} from '../component/app.component.directives';  
  5.   
  6. @Component({  
  7.   template: `<input type="text" onFocus>`  
  8. })  
  9. class TestHoverFocusComponent {  
  10. }  
  11.   
  12.   
  13. describe('Directive: onFocus', () => {  
  14.   
  15.   let component: TestHoverFocusComponent;  
  16.   let fixture: ComponentFixture<TestHoverFocusComponent>;  
  17.   let inputEl: DebugElement;  
  18.   
  19.   beforeEach(() => {  
  20.     TestBed.configureTestingModule({  
  21.       declarations: [TestHoverFocusComponent, FocusDirective]  
  22.     });  
  23.     fixture = TestBed.createComponent(TestHoverFocusComponent);  
  24.     component = fixture.componentInstance;  
  25.     inputEl = fixture.debugElement.query(By.css('input'));  
  26.   });  
  27.   
  28.   it('hovering over input', () => {  
  29.     inputEl.triggerEventHandler('mouseover'null);  
  30.     fixture.detectChanges();  
  31.     expect(inputEl.nativeElement.style.backgroundColor).toBe('red');  
  32.   
  33.     inputEl.triggerEventHandler('mouseout'null);  
  34.     fixture.detectChanges();  
  35.     expect(inputEl.nativeElement.style.backgroundColor).toBe('inherit');  
  36.   });  
  37. });  
The output is as below:

 
Unit Test of Angular Service
 
For performing the unit test on Angular service, we will create login authentication service called LoginService and wrap that service into a component called LoginComponent. 
 
Sample code of the app.service.login.ts
  1. export class LoginService {  
  2.     isAuthenticated(): boolean {  
  3.       return !!localStorage.getItem('token');  
  4.     }  
  5.   }  
Sample code of the app.component.login.ts
  1. import {Component} from '@angular/core';  
  2. import { LoginService } from "./app.service.login";  
  3.   
  4. @Component({  
  5.   selector: 'app-login',  
  6.   template: `<a [hidden]="needsLogin()">Login</a>`  
  7. })  
  8. export class LoginComponent {  
  9.   
  10.   constructor(private auth: LoginService) {  
  11.   }  
  12.   
  13.   needsLogin() {  
  14.     return !this.auth.isAuthenticated();  
  15.   }  
  16. }  
Sample code of the app.component.login.specs.ts
  1. import {LoginComponent} from '../component/app.component.loginservice';  
  2. import { LoginService } from "../component/app.service.login";  
  3.   
  4. describe('Component: Login', () => {  
  5.   
  6.   let component: LoginComponent;  
  7.   let service: LoginService;  
  8.   
  9.   beforeEach(() => {   
  10.     service = new LoginService();  
  11.     component = new LoginComponent(service);  
  12.   });  
  13.   
  14.   afterEach(() => {   
  15.     localStorage.removeItem('token');  
  16.     service = null;  
  17.     component = null;  
  18.   });  
  19.   
  20.   
  21. //   it('canLogin returns false when the user is not authenticated', () => {  
  22. //     expect(component.needsLogin()).toBeTruthy();  
  23. //   });  
  24.   
  25.   it('canLogin returns false when the user is not authenticated', () => {  
  26.     localStorage.setItem('token''12345');   
  27.     expect(component.needsLogin()).toBeFalsy();  
  28.   });  
  29. });  
The output is:

 
Now, in the above example, login component and login service are tightly coupled. If we make some changes in the storing of the token from local storage into any other forms like cookies, then our test will break since we are trying to set  itinto local storage. That's why we need isolation of our test class so that it does not fully depend on Login Components. We can also implement this concept by mocking our dependencies. Basically mocking is a technique of creating something which looks the same as the dependency but actually is different and we can control it.  Below is the example of the above test class of login component using mocking class: 
  1. import {LoginComponent} from '../component/app.component.loginservice';  
  2.   
  3. class MockAuthService {   
  4.   authenticated = false;  
  5.   
  6.   isAuthenticated() {  
  7.     return this.authenticated;  
  8.   }  
  9. }  
  10.   
  11. describe('Component: Login', () => {  
  12.   
  13.   let component: LoginComponent;  
  14.   let service: MockAuthService;  
  15.   
  16.   beforeEach(() => {   
  17.     service = new MockAuthService();  
  18.     component = new LoginComponent(service);  
  19.   });  
  20.   
  21.   afterEach(() => {  
  22.     service = null;  
  23.     component = null;  
  24.   });  
  25.   
  26.   
  27.   it('canLogin returns false when the user is not authenticated', () => {  
  28.     service.authenticated = false;   
  29.     expect(component.needsLogin()).toBeTruthy();  
  30.   });  
  31.   
  32.   it('canLogin returns false when the user is not authenticated', () => {  
  33.     service.authenticated = true;   
  34.     expect(component.needsLogin()).toBeFalsy();  
  35.   });  
  36. });  
So, that's all for this series. If you have any questions or doubts, do let me know and I will be happy to resolve your queries.