Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Unit Testing in Angular 4 Using Jasmine and Karma - Part 2

DZone's Guide to

Unit Testing in Angular 4 Using Jasmine and Karma - Part 2

Learn how to develop unit tests for Angular 4 components, directives, and services in part 2 of this tutorial series.

· DevOps Zone ·
Free Resource

Easily enforce open source policies in real time and reduce MTTRs from six weeks to six seconds with the Sonatype Nexus Platform. See for yourself - Free Vulnerability Scanner. 

Welcome back to the last part of this series, “Unit Testing Using Jasmine in Angular 4.” This is the second or last part of this series. In this section, we will discuss the below topics –

  • How to develop a Unit Test of Angular Components
  • How to develop a Unit Test of Angular Directives
  • How to develop a Unit Test of Angular Services

Now if you are reading this article for the first time, and missed the first part of the articles, then please note that in the first part, we discussed the below topics –

  • Basic Concept of Unit Tests
  • The available Frameworks or Tools for Unit Tests
  • Why we need to Perform Unit Tests
  • How to Set Up an Environment for Unit Tests
  • Perform a Unit Test for a TypeScript Basic Class
  • Perform a Unit Test of a Parameter Based Method in Class

Unit Test of Components

Now it’s time to develop a unit test program based on an Angular component. Before writing the unit test specs we need to write down an angular component first. So we develop an Angular component named StudentTemplateComponent, which is used to display the student information along with one Input() and one Output() property. For the component HTML template, we use an HTML template using Template Selector within the @Component decorator.

Sample code of Angular Component (StudentTemplateComponent) of app.component.template

import { Component, Input, Output, EventEmitter } from '@angular/core';

@Component({
    // moduleId: module.id,
    selector: 'student-template-data',
    template: `
                <div class="form-horizontal">
                <h2 class="aligncenter">Student Details (Template)</h2><br />
                <div class="row">
                    <div class="col-xs-12 col-sm-2 col-md-2">
                        <span>First Name</span>
                    </div>
                    <div class="col-xs-12 col-sm-4 col-md-4">
                        <input type="text" id="txtFName" placeholder="Enter First Name" #firstName/>
                    </div>
                    <div class="col-xs-12 col-sm-2 col-md-2">
                        <span>Last Name</span>
                    </div>
                    <div class="col-xs-12 col-sm-4 col-md-4">
                        <input type="text" id="txtLName" placeholder="Enter Second Name" #lastName/>
                    </div>
                </div>
                <br />
                <div class="row">

                    <div class="col-xs-12 col-sm-2 col-md-2">
                        <span>Email</span>
                    </div>
                    <div class="col-xs-12 col-sm-4 col-md-4">
                        <input type="email" id="txtEmail" #email/>
                    </div>
                    <div class="col-xs-12 col-sm-2 col-md-2">
                    </div>
                    <div class="col-xs-12 col-sm-4 col-md-4">
                        <input type="button" id="btnSubmit" value="Submit" class="btn btn-primary" [disabled]="!enabled" 
                                (click)="onSubmit(firstName.value,lastName.value,email.value)"/>
                    </div>
                </div>
            </div>
              `
})

export class StudentTemplateComponent {

    private _model: any = {};

    @Input() enabled:boolean = true;
    @Output() onFormSubmit: EventEmitter<any> = new EventEmitter<any>();

    constructor() {
    }

    private onSubmit(firstName:string, lastName:string, email : string): void {
        this._model.firstName = firstName;
        this._model.lastName = lastName;
        this._model.email = email;
        if (typeof (this._model) === "undefined") {
            alert("Form not Filled Up Properly");
        }
        else {
            alert("Data Is Correct");
            this.onFormSubmit.emit(this._model);
        }
    }

    private onClear(): void {
        this._model = {};
    }
}


Testing the @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 a selector or using a component instance as an object. So, normally, we can set the value of input properties as below in our unit test specs.

it('Setting value to the input properties variables", () => {
component.enabled = false;
});

If we want to validate the value of the input properties in the components, then we can write the specs code as below.

it('Setting value to input properties on button click', () => {

component.enabled = false;

fixture.detectChanges();

expect(submitEl.nativeElement.disabled).toBeTruthy();

});


In both of the above blocks, we used fixture.detectChanges() to fire the change detection event and update the View as per the value.

Testing the @Output() Decorator

Testing 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.

Now, create another TypeScript file for defining the specs for the above class file.

Sample code of app.component.template.spec.ts

import {TestBed, ComponentFixture, inject, async} from '@angular/core/testing';
import { StudentTemplateComponent } from '../component/app.component.template';
import {Component, DebugElement} from "@angular/core";
import {By} from "@angular/platform-browser";


describe('Component: Student Form ', () => {

  let component: StudentTemplateComponent;
  let fixture: ComponentFixture<StudentTemplateComponent>;
  let submitEl: DebugElement;
  let firstNameEl: DebugElement;
  let lastNameEl: DebugElement;
  let emailEl : DebugElement;

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [ StudentTemplateComponent ]
    });

    fixture = TestBed.createComponent(StudentTemplateComponent);
    component = fixture.componentInstance;

    submitEl = fixture.debugElement.query(By.css('input[id=btnSubmit]'));
    firstNameEl = fixture.debugElement.query(By.css('input[id=txtFName]'));
    lastNameEl = fixture.debugElement.query(By.css('input[id=txtLName]'));
    emailEl = fixture.debugElement.query(By.css('input[type=email]'));
  }));

  it('Setting value to input properties on form load', () => {
    component.enabled = false;
    fixture.detectChanges();
    expect(submitEl.nativeElement.disabled).toBeTruthy();
  });

  it('Setting value to input properties on button click', () => {
    component.enabled = true;
    fixture.detectChanges();
    expect(submitEl.nativeElement.disabled).toBeFalsy();
  });

  it('Entering value in input controls and emit output events', () => {
    let user: any;
    firstNameEl.nativeElement.value = "Debasis";
    lastNameEl.nativeElement.value = "Saha";
    emailEl.nativeElement.value = "debasis@yahoo.com";

    // Subscribe to the Observable and store the user in a local variable.
    component.onFormSubmit.subscribe((value) => user = value);

    // This sync emits the event and the subscribe callback gets executed above
    submitEl.triggerEventHandler('click', null);

    // Now we can check to make sure the emitted value is correct
    expect(user.firstName).toBe("Debasis");
    expect(user.lastName).toBe("Saha");
    expect(user.email).toBe("debasis@yahoo.com");
  });
});

Now, run the index.html file in the browser.

Image title

Unit Test of an Angular Attribute Directive

In the above section, we discuss how to perform a unit test on an Angular component. In the Angular framework, one of most popular and used parts is directives, just like components, so it is important for us in our development environment to provide feasibility to perform unit tests on a directive. For this purpose, we first develop a directive called FocusDirectives, which is basically first an event on mouse-over that changes the DOM element color to red, against which this directive is used. So, as per our knowledge, we need to define @HostListener to capture the mouse-related events of any input elements against which these directives used. At the same time, we can also use the @HostBinding to use the style attribute of the input elements.

Sample code of directives

import {
    Directive,
    HostListener,
    HostBinding
} from '@angular/core';

@Directive({
  selector: '[onFocus]'
})
export class FocusDirective {

  @HostBinding("style.background-color") backgroundColor: string;

  @HostListener('mouseover') onHover() {
    this.backgroundColor = 'red';
  }

  @HostListener('mouseout') onLeave() {
    this.backgroundColor = 'inherit';
  }
}


Sample code of the unit test specs of the directives

import {TestBed, ComponentFixture} from '@angular/core/testing';
import {Component, DebugElement} from "@angular/core";
import {By} from "@angular/platform-browser";
import {FocusDirective} from '../component/app.component.directives';

@Component({
  template: `<input type="text" onFocus>`
})
class TestHoverFocusComponent {
}


describe('Directive: onFocus', () => {

  let component: TestHoverFocusComponent;
  let fixture: ComponentFixture<TestHoverFocusComponent>;
  let inputEl: DebugElement;

  beforeEach(() => {
    TestBed.configureTestingModule({
      declarations: [TestHoverFocusComponent, FocusDirective]
    });
    fixture = TestBed.createComponent(TestHoverFocusComponent);
    component = fixture.componentInstance;
    inputEl = fixture.debugElement.query(By.css('input'));
  });

  it('hovering over input', () => {
    inputEl.triggerEventHandler('mouseover', null);
    fixture.detectChanges();
    expect(inputEl.nativeElement.style.backgroundColor).toBe('red');

    inputEl.triggerEventHandler('mouseout', null);
    fixture.detectChanges();
    expect(inputEl.nativeElement.style.backgroundColor).toBe('inherit');
  });
});

Now, run the index.html file in the browser.

Image title

Unit Test of Angular Services

Now, just like directives, one of the most important and reused parts of the Angular framework is Services. To perform unit tests on Angular service, we will create a login authentication service called LoginService and wrap that service into a component called LoginComponent.

Sample code of the app.service.login.ts

export class LoginService {
    isAuthenticated(): boolean {
      return !!localStorage.getItem('token');
    }
  }

Sample code of the app.component.login.ts

import {Component} from '@angular/core';
import { LoginService } from "./app.service.login";

@Component({
  selector: 'app-login',
  template: `<a [hidden]="needsLogin()">Login</a>`
})
export class LoginComponent {

  constructor(private auth: LoginService) {
  }

  needsLogin() {
    return !this.auth.isAuthenticated();
  }
}

Sample code of the app.component.login.specs.ts

import {LoginComponent} from '../component/app.component.loginservice';
import { LoginService } from "../component/app.service.login";

describe('Component: Login', () => {

  let component: LoginComponent;
  let service: LoginService;

  beforeEach(() => { 
    service = new LoginService();
    component = new LoginComponent(service);
  });

  afterEach(() => { 
    localStorage.removeItem('token');
    service = null;
    component = null;
  });


//   it('canLogin returns false when the user is not authenticated', () => {
//     expect(component.needsLogin()).toBeTruthy();
//   });

  it('canLogin returns false when the user is not authenticated', () => {
    localStorage.setItem('token', '12345'); 
    expect(component.needsLogin()).toBeFalsy();
  });
});

The output is:

Image title

Now, in the above example, the 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 it into 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 a mocking class:

import {LoginComponent} from '../component/app.component.loginservice';

class MockAuthService { 
  authenticated = false;

  isAuthenticated() {
    return this.authenticated;
  }
}

describe('Component: Login', () => {

  let component: LoginComponent;
  let service: MockAuthService;

  beforeEach(() => { 
    service = new MockAuthService();
    component = new LoginComponent(service);
  });

  afterEach(() => {
    service = null;
    component = null;
  });


  it('canLogin returns false when the user is not authenticated', () => {
    service.authenticated = false; 
    expect(component.needsLogin()).toBeTruthy();
  });

  it('canLogin returns false when the user is not authenticated', () => {
    service.authenticated = true; 
    expect(component.needsLogin()).toBeFalsy();
  });
});

So, that's all for this article. If you have any questions or doubts, do let me know and I will be happy to resolve your queries.

Automate open source governance at scale across the entire software supply chain with the Nexus Platform. Learn more.

Topics:
angular 4 ,unit testing ,jasmine ,karma ,devops ,tutorial

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}