In this tutorial, we will go through the steps for testing Angular Component by using the Jest framework.
Since Angular is a widely used front-end application development framework, it is the responsibility of each developer to make sure that the components are implemented as per the requirements of the project.
Unit Testing is one of the recommended approaches of guaranteeing the quality of the application.
What is Unit Testing?
Unit testing is an approach of software testing where individual units of the code are tested against the test data. The purpose of unit testing is to validate that each unit of the software performs operations as per expectations. Unit testing involves the lowest cost because each test targets to a limited scope. Unit tests are fast and less time consuming.
Understanding the Angular Testing Object Model
One of the best advantages of using Angular is that it provides the Testing Objects model. Now the question is why is this so important?
The reason behind this is the execution of an Angular application. The Angular application is executed using the NgModule environment configuration. This means that, all components and their dependencies e.g. standard module like FormsModule and other Angular Services, are finally managed by NgModule; so naturally when we try to test the component, we need a similar environment to instantiate component with its dependencies.
In Angular, to define the testing environment, we have the 'TestBed' class. This class is used to configure and initialize environment for unit testing. Figure 1 will give you an idea of the TestBed Class
As shown in figure 1, the TestBed class provides an instance for Angular Components and Services with testing module so that the test can run successfully.
ComponentFixture
This class is used for debugging and testing a component. This class contains the componentInstance. This property is used to provide an instance of the root component class.
The nativeElement property of this class represents the native element at the root of the component.
The detectChanges() method is used to trigger a change detection cycle for the component. This method will be executed when the test is used to trigger and event while testing the component. The destroy() method is triggered while the component is destructed.
Using Jest to test Angular Component
Jest is JavaScript testing framework. It was created by Facebook engineers. We can use Jest with React, Angular, Vue, Node, etc.
Jest has the following advantages:
- Zero Config.
- Tests are executed parallel in their own processes to maximize performance, so each test is isolated.
- Since tests are executed in parallel, they are fast safe.
- Jest is well documented with a rich set of APIs.
- It is easy to generate code coverage by using --coverage switch.
- Jest uses headless JSDOM, the JSDOM is a JavaScript headless browser that is used to create realistic testing environment.
Using Jest for Angular testing
- Angular CLI
- Node.js
- Visual Studio Code
"extends":
"./tsconfig.base.json",
"compilerOptions": {
"outDir":
"./out-tsc/spec",
"types": [
"jasmine"
]
},
"files": [
"src/test.ts",
"src/polyfills.ts"
],
"include": [
"src/**/*.spec.ts",
"src/**/*.d.ts"
]
}
"extends":
"./tsconfig.base.json",
"compilerOptions": {
"outDir":
"./out-tsc/spec",
"types": [
"jest",
"node"
],
"esModuleInterop": true,
"experimentalDecorators": true
},
"files": [
"src/window-mock.ts",
"src/polyfills.ts"
],
"include": [
"src/**/*.spec.ts",
"src/**/*.d.ts"
]
}
//
write the jest initialization for testing the angular w/o DOM
import
'jest-preset-angular';
// HTML Template parsing using docType
Object.defineProperty(document,
'doctype', {
value: '<!DOCTYPE html>'
});
Object.defineProperty(document.body.style, 'transform', {
value: () => {
return {
enumerable: true,
configurable: true
};
}
const { pathsToModuleNameMapper } = require('ts-jest/utils'); // load all settings from the TypeScript configuration const { compilerOptions } = require('./tsconfig.app.json'); module.exports = { preset: 'jest-preset-angular', // load the adapater roots: ['Listing 5: The Jest Configuration/src/'], // start searching for files from root testMatch: ['**/+(*.)+(spec).+(ts|js)'], // test file extensions setupFilesAfterEnv: [' /src/window-mock.ts'], // setup env file collectCoverage: true, // code coverage coverageReporters: ['html'], // generate the report in HTML coverageDirectory: 'coverage/my-ng-app', // folder for coverage moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths || {}, { prefix: ' /' }) };
- The Jest configuration file reads compileOptions from the tsconfig.app.json. This file defines TypeScript compilation specifications.
- The .spect.ts files will be read so that Unit Tests developed using jest object model will be trsnapiled.
- The JSDOM environment will be read from the window-mock.ts.
- The coverage configuration will be set so that the coverage files will be generated in the coverage folder.
constructor( public ProductRowId: number, public ProductId: string, public ProductName: string, public CategoryName: string, public Manufacturer: string, public Description: string, public BasePrice: number ){} } export const Categories = [ 'Electronics', 'Electrical', 'Food' ]; export const Manufacturers = [ 'HP', 'IBM', 'Bajaj', 'Phillipse', 'Parle', 'TATA' ];Listing 6: The Product class and other constants
export class Logic { private products: ArrayListing 7: The logic class; constructor(){ this.products = new Array (); } getProducts(): Array { this.products.push(new Product(1, 'Prd001', 'Laptop', 'Electronics', 'HP', 'Gaming', 120000)); this.products.push(new Product(2, 'Prd002', 'Iron', 'Electrical', 'Bajaj', 'Cotton Friendly', 3000)); this.products.push(new Product(3, 'Prd003', 'Biscuits', 'Food', 'Parle', 'Glucose', 10)); return this.products; } addProduct(prd: Product): Array { this.products.push(prd); return this.products; } }
import { Component, OnInit } from '@angular/core'; import {Product,Manufacturers, Categories} from './../app.product.model'; import {Logic} from './../logic'; @Component({ selector: 'app-productform-component', templateUrl: './app.productform.view.html' }) // OnInit: Angular Component's lifecycle interface export class ProductFormComponent implements OnInit { product: Product; products: Array; categories = Categories; manufacturers = Manufacturers; private logic: Logic; columnHeaders: Array ; tax: number; constructor() { this.product = new Product(0, '', '', '', '', '', 0); this.products = new Array (); this.logic = new Logic(); this.columnHeaders = new Array (); this.tax = 0; } ngOnInit(): void { this.products = this.logic.getProducts(); console.log(JSON.stringify(this.products)); // read properties from product object for (const p of Object.keys(this.product)) { this.columnHeaders.push(p); } console.log(JSON.stringify(this.columnHeaders)); } clear(): void { this.product = new Product(0, '', '', '', '', '', 0); } save(): void { this.tax = this.product.BasePrice * 0.2; this.products = this.logic.addProduct(this.product); console.log(JSON.stringify(this.products)); } getSelectedProduct(event): void { this.product = Object.assign({}, event); } }
The Product Form Component
<table> <tbody><tr> <td> <div class="container"> <form name="frmProduct"> <div class="container"> <div class="form-group"> <label>Product Row Id</label> <input class="form-control" name="ProductRowId" [(ngModel)]="product.ProductRowId" type="text" /> </div> <div class="form-group"> <label>Product Id</label> <input class="form-control" name="ProductId" [(ngModel)]="product.ProductId" type="text" />
</div> <div class="form-group"> <label>Product Name</label> <input class="form-control" name="ProductName" [(ngModel)]="product.ProductName" type="text" />
</div> <div class="form-group"> <label>Category Name</label> <select class="form-control" name="CategoryName" [(ngModel)]="product.CategoryName="">
<option ngfor="let c of categories" value="">{{c}}</option> </select> </div> <div class="form-group"> <label>Manufacturer</label> <select class="form-control" name="Manufacturer" [(ngModel)]="product.Manufacturer>
<option ngfor="let m of Manufacturers" value="">{{m}}</option> </select> </div> </div> <div class="form-group"> <label>Description</label> <input class="form-control" name="Description" [(ngModel)]="product.Description" type="text" />
</div> <div class="form-group"> <label>Base Price</label> <input class="form-control" name="BasePrice" [(ngModel)]="product.BasePrice" type="text" />
<input class="form-control" disabled name="tax" type="text" [value]="tax" /> </div> <div class="form-group"> <input class="btn btn-warning" (click)="clear()" type="button" value="Clear" /> <input class="btn btn-success" (click)="save()" type="button" value="Save" /> </div> </form> </div> </td> </tr> </tbody></table>
// collect all required testing objects for Angular App import { TestBed, ComponentFixture, async } from "@angular/core/testing"; // for Two-Way binding import { FormsModule } from '@angular/forms'; // import component to be tested and its dependencies import { Product} from './../app.product.model'; import { ProductFormComponent } from './app.productform.component'; // define the test suit describe('ProductFormComponent', () => { // dfefine the required objects fot test let component: ProductFormComponent; // defining the Component Fixture to monitor changed in component // e.g. DataBinding changes let fixture: ComponentFixture; // define the HTML element let button: HTMLElement; // define the test env. so that the test will be // using Angular standard modules to execute test on component beforeEach(() => { // defin the TestBedConfiguration TestBed.configureTestingModule({ declarations: [ProductFormComponent], imports: [FormsModule] }).compileComponents(); // the component will be compiled // (includes HTML Tremplate) }); // definition for all objects before test starts beforeEach(() => { // initiaze the fixture so that the component 'selector' // and its HTML template will be initialized fixture = TestBed.createComponent(ProductFormComponent); // read the component's instace to execute method in it component = fixture.componentInstance; // detect the first databinding changes fixture.detectChanges(); }); // the test case it('should calculate tax based on base price when save button is clicked', () => { // define the product instance const product = new Product(0, '', '', '', '', '', 0); console.log(`Conponent instance ${component}`); product.BasePrice = 4000; component.product = product; // receive the nativeElement for HTML Template DOM const element = fixture.nativeElement; // recive the button button = element.querySelector('.btn-success'); // define an event // when the button dispatch the click event the // 'save()' method of the component will be called const eventType = button.dispatchEvent(new Event('click')); // detect any changed in HTML DOM against the dispatched event fixture.detectChanges(); // asser the value in the disabled text element expect(element.querySelector('input[disabled]').value).toEqual('800'); }); });
No comments:
Post a Comment