How To Perform Unit Test Using Jasmine In Angular 2 - Day Twenty Eight

In this Angular 2.0 article series, we have already discussed about different types of basic and advanced concepts or features of AngularJS 2.0 like Data binding, Directives, pipes, Service, route, http modules, pipes, change detection, state management, Lazy loading of the modules, Localization including implementations of amcharts library in AngularJS 2.0. Now, in this article, we will discuss about one of the main advantage of angularjs i.e. Unit Testing.

In case you have not had a look at the previous articles of this series, go through the links mentioned below.

Nowadays, JavaScript has become the de facto programming language to build and empower frontend/ Web AZpplications. We can use JavaScript to develop simple or complex applications. However, applications in production are often vulnerable to bugs caused by design inconsistencies, logical implementation errors, and similar issues. For this reason, it is usually difficult to predict how applications will behave in real-time environments, which leads to an unexpected behavior, non-availability of the Applications, or outages for short or long durations. This generates lack of confidence and dissatisfaction among application users. Also, high cost is often associated with fixing the production bugs. Therefore, there is a need to develop applications that are of a high quality and that offer high availability.

Test-Driven-Development is an engineering process in which the developer writes an initial automated test case that defines a feature, subsequently writes the minimum amount of code to pass the test and eventually refactors the code to the acceptable standards.

A Unit test is used to test individual components of the system. An integration test is a test, which tests the system as a whole and how it will run in production. Unit tests should only verify the behavior of a specific unit of code. If the unit's behavior is modified, then the unit test would be updated as well. Unit tests should not make assumptions about the behavior of other parts of your codebase or your dependencies. When other parts of your codebase are modified, your Unit tests should not fail. (Any failure indicates a test that relies on other components and is therefore not a Unit test.) Unit tests are cheap to maintain and should only be updated when the individual units are modified. For TDD in Angular, a unit is most commonly defined as a class, pipe, component or Service. It is important to keep units relatively small. This helps you to write small tests, which are self-documenting, where they are easy to read and understand.

The Testing Tool chain

Our testing toolchain will consist of the tools given below.

  • Jasmine
  • Karma
  • Phantom-js
  • Istanbul
  • Sinon
  • Chai

Jasmine is the most popular testing framework in Angular community. This is the Core framework that we will write our Unit tests with.

Karma is a test automation tool for controlling the execution of our tests and what Browser to perform them under. It also allows us to generate various reports on the results. For one or two tests, this may seem like overkill, but as an Application grows larger and the number of units to test grows, it is important to organize, execute and report on the tests in an efficient manner. Karma is library agnostic, so we can use other testing frameworks in combination with other tools (like code coverage reports, spy testing, e2e etc.)

In order to test our Angular Application, we must create an environment for it to run in. We can use a Browser like Chrome or Firefox to accomplish this (Karma supports in-Browser testing) or we can use a Browser-less environment to test our Application, which can offer us greater control over automating certain tasks and managing our testing Workflow. PhantomJS provides a JavaScript API that allows us to create a headless DOM instance, which can be used to Bootstrap our Angular Application. Use this DOM instance that is running our Angular Application. We can run our tests.

Istanbul is used by Karma to generate code coverage reports, which tells us the overall percentage of our Application being tested. This is a great way to track which components/Services/ pipes etc. have tests written and which don't. We can get some useful insights into how much of the Application is being tested and where.

For some extra testing functionality, we can use the Sinon library for things like test spys, test subs and mock XHR requests. This is not necessarily required as Jasmine comes with the spyOn function for incorporating spy tests.

Chai is an assertion library that can be paired with any other testing framework. It offers some syntactic sugar that lets us write our unit tests with different verbiage - we can use a should, expect or assert interface. Chai also takes advantage of function chaining to form English-like sentences used to describe tests in a more user friendly way. Chai isn't a required library for testing and we won't explore it much more in this handout, but it is a useful tool to create cleaner, more well-written tests.

Filename conventions

Each Unit test is put into its own separate file. Angular team recommends putting Unit test scripts alongside the files. They are testing and using a .spec filename extension to mark it as a testing script (this is a Jasmine convention). Hence, if you had a component /app/components/mycomponent.ts, then your Unit test for this component would be in /app/components/mycomponent.spec.ts. This is a matter of personal preference; you can put your testing scripts wherever you like, though keeping them close to your source files makes them easier to find and gives those, who aren't familiar with the source code an idea of how that particular piece of code should work.

How to install Jasmine on Windows

To install Jasmine on Windows machine, we need to first install node.js. Since we are using TypeScript to develop Angular 2.0 code, which means Node JS environment is already set in the development machine. Thus, now we can directly install Jasmine by using command line arguments.

Step 1

npm -- version (for checking npm is properly installed or not).

Step 2

npm install –g yo (for install yomen).


Step 3

npm install –g bower (for install bower).

Step 4

npm install –g generator-jasmine jasmine-core (to install Jasmine).


The –g switch asked npm to install the packages within node.js global modules directory. To finalize testing environment, we need to scaffold Jasmine’s Test directory. For this, go to the your project location path, followed by running the command given below. This command creates a test Directive and within this Directive, create a index.html file and a spec folder for storing the spec related JS file within the Directive. The index.html file is test runner or spec runner file of Jasmine.

Step 5

yo jasmine

There is another way of using Jasmine. For it, we can install karma framework, using npm by using the command given below.

npm install –g karma karma-cli jasmine-core

Now, after it, go the project directory by browsing and initializing the configuration. For this karma, ask few simple questions and on the basis of the given answer, it will create the configuration files.

karma init jasmine.config.js

After completing the configuration file creation, we just need to start the karma by passing the configuration file name as the arguments.

karma start jasmine.config.js

Now, the environment to run theJasmine test is ready. Now, there is a need to run Jasmime test. For this, we create a folder named test within the project directive and add one html file called index.html for running the jasmine test file. Now first we will create a simple JavaScript function which takes two number arguments and return the total of two numbers. For this, we need to add another JavaScript file within the same folder named sampletest.js and write down the below code –

Also, we need to another JavaScript file to write down the code of Unit test. For this, we add another JS file test.js and write down the code given below. In this file, we add two scenarios where one is for correct scenario and another is wrong scenario i.e. expected and given data does not match.

Now, we add need to add the code given below in the index.html file.

When I write Unit tests, I follow a pattern called arrange/act/assert (A/A/A). Arrange refers to the process of setting up the scenario required for the test. Act refers to performing the test itself and assert refers to checking the result to make sure it is correct. Jasmine tests are written, using JavaScript functions, which makes writing tests a nice extension of writing an Application code. There are several Jasmine functions in the example, which I have described below.

Name

Descriptions

describe

Groups a number of the related tests (This is optional, but it helps to organize test code).

beforeEach

Executes a function before each test (This is often used for the arrange part of a test).

it

Executes a function to form a test (The act part of the test).

expect

Identifies the result from the test (part of the assert stage).

toEqual

Compares the result from the test to the expected value (the other part of the assert).

The basic sequence to pay attention to is that the function executes a test function, so that the expect and toEqual functions can be used to assess the result. The toEqual function is only one way that Jasmine can evaluate the result of a test. I have listed the other available functions given below.

Name

Descriptions

expect(x).toEqual(val)

Asserts that x has the same value as val (but not necessarily the same object).

expect(x).toBe(obj)

Asserts that x and obj are the same object.

expect(x).toMatch(regexp)

Asserts that x matches the specified regular expression.

expect(x).toBeDefined()

Asserts that x has been defined.

expect(x).toBeUndefined()

Asserts that x has not been defined.

expect(x).toBeNull()

Asserts that x is null.

expect(x).toBeTruthy()

Asserts that x is true or evaluates to true.

expect(x).toBeFalsy()

Asserts that x is false or evaluates to false.

expect(x).toContain(y)

Asserts that x is a string that contains y.

expect(x).toBeGreaterThan(y)

Asserts that x is greater than y.

To perform a simple unit test, I first write down a simple TypeScript function, which adds two numbers.

Now, write down the code given below, as per the file.
 
tsconfig.js
  1. {  
  2.   "compilerOptions": {  
  3.     "target""es5",  
  4.     "module""commonjs",  
  5.     "moduleResolution""node",  
  6.     "sourceMap"true,  
  7.     "emitDecoratorMetadata"true,  
  8.     "experimentalDecorators"true,  
  9.     "removeComments"false,  
  10.     "noImplicitAny"true,  
  11.     "suppressImplicitAnyIndexErrors"true  
  12.   }  
  13. }  
System.config.js
  1. /** 
  2.  * PLUNKER VERSION 
  3.  * (based on systemjs.config.js in angular.io) 
  4.  * System configuration for Angular 2 samples 
  5.  * Adjust as necessary for your application needs. 
  6.  */  
  7. (function (global) {  
  8.   
  9.     var ngVer = '@2.0.0'// lock in the angular package version; do not let it float to current!  
  10.     var routerVer = '@3.0.0'// lock router version  
  11.     var formsVer = '@0.3.0'// lock forms version  
  12.   
  13.     //map tells the System loader where to look for things  
  14.     var map = {  
  15.         'app''app',  
  16.         '@angular''https://unpkg.com/@angular', // sufficient if we didn't pin the version  
  17.         '@angular/forms''https://unpkg.com/@angular/forms' + formsVer,  
  18.         '@angular/router''https://unpkg.com/@angular/router' + routerVer,  
  19.         'angular2-in-memory-web-api''https://unpkg.com/angular2-in-memory-web-api', // get latest  
  20.         'rxjs''https://unpkg.com/rxjs@5.0.0-beta.12',  
  21.         'ts''https://unpkg.com/plugin-typescript@4.0.10/lib/plugin.js',  
  22.         'typescript''https://unpkg.com/typescript@2.0.2/lib/typescript.js',  
  23.     };  
  24.   
  25.     //packages tells the System loader how to load when no filename and/or no extension  
  26.     var packages = {  
  27.         'app': { main: 'main.ts', defaultExtension: 'ts' },  
  28.         'rxjs': { defaultExtension: 'js' },  
  29.         'angular2-in-memory-web-api': { main: 'index.js', defaultExtension: 'js' },  
  30.     };  
  31.   
  32.     var ngPackageNames = [  
  33.       'core',  
  34.       'common',  
  35.       'compiler',  
  36.       'platform-browser',  
  37.       'platform-browser-dynamic',  
  38.       'http',  
  39.       'upgrade',  
  40.     ];  
  41.   
  42.     var ngTestingPackageNames = [  
  43.       'core',  
  44.       'common',  
  45.       'compiler',  
  46.       'platform-browser',  
  47.       'platform-browser-dynamic',  
  48.       'router'  
  49.     ];  
  50.   
  51.     // Add map entries for each angular package  
  52.     // only because we're pinning the version with `ngVer`.  
  53.     ngPackageNames.forEach(function (pkgName) {  
  54.         map['@angular/' + pkgName] = 'https://unpkg.com/@angular/' + pkgName + ngVer;  
  55.     });  
  56.   
  57.     // Add package entries for angular packages with special versions  
  58.     ngPackageNames = ngPackageNames.concat(['forms''router']);  
  59.   
  60.     // Add map entries for each angular testing module  
  61.     ngTestingPackageNames.forEach(function (pkgName) {  
  62.         map['@angular/' + pkgName + '/testing'] = 'https://unpkg.com/@angular' + '/' + pkgName + ngVer + /bundles/ + pkgName + '-testing.umd.js';  
  63.     });  
  64.   
  65.     // Individual files (~300 requests):  
  66.     function packIndex(pkgName) {  
  67.         packages['@angular/' + pkgName] = { main: 'index.js', defaultExtension: 'js' };  
  68.     }  
  69.   
  70.     // Bundled (~40 requests):  
  71.     function packUmd(pkgName) {  
  72.         packages['@angular/' + pkgName] = { main: '/bundles/' + pkgName + '.umd.js', defaultExtension: 'js' };  
  73.     }  
  74.   
  75.     // Most environments should use UMD; some (e.g. Karma) need the individual index files  
  76.     var setPackageConfig = System.packageWithIndex ? packIndex : packUmd;  
  77.   
  78.     // Add package entries for angular packages  
  79.     ngPackageNames.forEach(setPackageConfig);  
  80.   
  81.     var config = {  
  82.         // DEMO ONLY! REAL CODE SHOULD NOT TRANSPILE IN THE BROWSER  
  83.         transpiler: 'ts',  
  84.         typescriptOptions: {  
  85.             tsconfig: true  
  86.         },  
  87.         meta: {  
  88.             'typescript': {  
  89.                 "exports"'ts'  
  90.             }  
  91.         },  
  92.         map: map,  
  93.         packages: packages  
  94.     };  
  95.   
  96.     System.config(config);  
  97.   
  98. })(this);  
  99.   
  100.   
  101. /* 
  102. Copyright 2016 Google Inc. All Rights Reserved. 
  103. Use of this source code is governed by an MIT-style license that 
  104. can be found in the LICENSE file at http://angular.io/license 
  105. */  
bower-test-shim.js
  1. (function () {  
  2.     Error.stackTraceLimit = 0; // "No stacktrace"" is usually best for app testing.  
  3.   
  4.     // Uncomment to get full stacktrace output. Sometimes helpful, usually not.  
  5.     // Error.stackTraceLimit = Infinity; //  
  6.   
  7.     jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000;  
  8.   
  9.     var baseURL = document.baseURI;  
  10.     baseURL = baseURL + baseURL[baseURL.length - 1] ? '' : '/';  
  11.   
  12.     System.config({  
  13.         baseURL: baseURL,  
  14.         // Extend usual application package list with test folder  
  15.         packages: { 'test': { main: 'index.js', defaultExtension: 'js' } },  
  16.         packageWithIndex: true // sadly, we can't use umd packages (yet?)  
  17.     });  
  18.   
  19.     //System.import(baseURL + 'systemjs.config.js')  
  20.     System.import('systemjs.config.js')  
  21.       .then(function () {  
  22.           return Promise.all([  
  23.             System.import('@angular/core/testing'),  
  24.             System.import('@angular/platform-browser-dynamic/testing')  
  25.           ])  
  26.       })  
  27.   
  28.       .then(function (providers) {  
  29.           var testing = providers[0];  
  30.           var testingBrowser = providers[1];  
  31.   
  32.           testing.TestBed.initTestEnvironment(  
  33.             testingBrowser.BrowserDynamicTestingModule,  
  34.             testingBrowser.platformBrowserDynamicTesting());  
  35.       })  
  36.   
  37.       // Import the spec files defined in the html (__spec_files__)  
  38.       .then(function () {  
  39.           console.log('loading spec files: ' + __spec_files__.join(', '));  
  40.           return Promise.all(  
  41.             __spec_files__.map(function (spec) {  
  42.                 return System.import(spec);  
  43.             }));  
  44.       })  
  45.   
  46.       //  After all imports load,  re-execute `window.onload` which  
  47.       //  triggers the Jasmine test-runner start or explain what went wrong  
  48.       .then(success, console.error.bind(console));  
  49.   
  50.     function success() {  
  51.         console.log('Spec files loaded; starting Jasmine testrunner');  
  52.         window.onload();  
  53.     }  
  54.   
  55. })();  
Index.html
  1. <!DOCTYPE html>  
  2. <html>  
  3. <head>  
  4.     <script>document.write('<base href="' + document.location + '" />');</script>  
  5.     <title>Angular 2  - Sample Unit Test</title>  
  6.     <meta charset="UTF-8">  
  7.     <meta name="viewport" content="width=device-width, initial-scale=1">  
  8.     <link href="styles/styles.css" rel="stylesheet" />  
  9.     <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/jasmine/2.4.1/jasmine.css">  
  10. </head>  
  11. <body>  
  12.     <script src="https://unpkg.com/systemjs@0.19.27/dist/system.src.js"></script>  
  13.     <script src="https://cdnjs.cloudflare.com/ajax/libs/jasmine/2.4.1/jasmine.js"></script>  
  14.     <script src="https://cdnjs.cloudflare.com/ajax/libs/jasmine/2.4.1/jasmine-html.js"></script>  
  15.     <script src="https://cdnjs.cloudflare.com/ajax/libs/jasmine/2.4.1/boot.js"></script>  
  16.     <script src="https://unpkg.com/reflect-metadata@0.1.3"></script>  
  17.     <script src="https://unpkg.com/zone.js@0.6.25?main=browser"></script>  
  18.     <script src="https://unpkg.com/zone.js@0.6.25/dist/proxy.js?main=browser"></script>  
  19.     <script src="https://unpkg.com/zone.js@0.6.25/dist/sync-test.js?main=browser"></script>  
  20.     <script src="https://unpkg.com/zone.js@0.6.25/dist/async-test.js?main=browser"></script>  
  21.     <script src="https://unpkg.com/zone.js@0.6.25/dist/fake-async-test.js?main=browser"></script>  
  22.     <script src="https://unpkg.com/zone.js@0.6.25/dist/jasmine-patch.js?main=browser"></script>  
  23.     <script src="systemjs.config.js"></script>  
  24.     <script src="test/message.spec.js"></script>  
  25.     <script>  
  26.     var __spec_files__ = [  
  27.       'test/message.spec',  
  28.      ];  
  29.     </script>  
  30.     <script src="bowser-test-shim.js"></script>  
  31. </body>  
  32. </html>  
Style.css
  1. /* Master Styles */  
  2. h1 {  
  3.   color: #369;  
  4.   font-family: Arial, Helvetica, sans-serif;  
  5.   font-size: 250%;  
  6. }  
  7. h2, h3 {  
  8.   color: #444;  
  9.   font-family: Arial, Helvetica, sans-serif;  
  10.   font-weight: lighter;  
  11. }  
  12. body {  
  13.   margin: 2em;  
  14. }  
  15. body, input[text], button {  
  16.   color: #888;  
  17.   font-family: Cambria, Georgia;  
  18. }  
  19. a {  
  20.   cursor: pointer;  
  21.   cursor: hand;  
  22. }  
  23. button {  
  24.   font-family: Arial;  
  25.   background-color: #eee;  
  26.   border: none;  
  27.   padding: 5px 10px;  
  28.   border-radius: 4px;  
  29.   cursor: pointer;  
  30.   cursor: hand;  
  31. }  
  32. button:hover {  
  33.   background-color: #cfd8dc;  
  34. }  
  35. button:disabled {  
  36.   background-color: #eee;  
  37.   color: #aaa;  
  38.   cursor: auto;  
  39. }  
  40.   
  41. /* Navigation link styles */  
  42. nav a {  
  43.   padding: 5px 10px;  
  44.   text-decoration: none;  
  45.   margin-top: 10px;  
  46.   display: inline-block;  
  47.   background-color: #eee;  
  48.   border-radius: 4px;  
  49. }  
  50. nav a:visited, a:link {  
  51.   color: #607D8B;  
  52. }  
  53. nav a:hover {  
  54.   color: #039be5;  
  55.   background-color: #CFD8DC;  
  56. }  
  57. nav a.active {  
  58.   color: #039be5;  
  59. }  
  60.   
  61. /* items class */  
  62. .items {  
  63.   margin: 0 0 2em 0;  
  64.   list-style-type: none;  
  65.   padding: 0;  
  66.   width: 24em;  
  67. }  
  68. .items li {  
  69.   cursor: pointer;  
  70.   position: relative;  
  71.   left: 0;  
  72.   background-color: #EEE;  
  73.   margin: .5em;  
  74.   padding: .3em 0;  
  75.   height: 1.6em;  
  76.   border-radius: 4px;  
  77. }  
  78. .items li:hover {  
  79.   color: #607D8B;  
  80.   background-color: #DDD;  
  81.   left: .1em;  
  82. }  
  83. .items li.selected:hover {  
  84.   background-color: #BBD8DC;  
  85.   color: white;  
  86. }  
  87. .items .text {  
  88.   position: relative;  
  89.   top: -3px;  
  90. }  
  91. .items {  
  92.   margin: 0 0 2em 0;  
  93.   list-style-type: none;  
  94.   padding: 0;  
  95.   width: 24em;  
  96. }  
  97. .items li {  
  98.   cursor: pointer;  
  99.   position: relative;  
  100.   left: 0;  
  101.   background-color: #EEE;  
  102.   margin: .5em;  
  103.   padding: .3em 0;  
  104.   height: 1.6em;  
  105.   border-radius: 4px;  
  106. }  
  107. .items li:hover {  
  108.   color: #607D8B;  
  109.   background-color: #DDD;  
  110.   left: .1em;  
  111. }  
  112. .items li.selected {  
  113.   background-color: #CFD8DC;  
  114.   color: white;  
  115. }  
  116.   
  117. .items li.selected:hover {  
  118.   background-color: #BBD8DC;  
  119. }  
  120. .items .text {  
  121.   position: relative;  
  122.   top: -3px;  
  123. }  
  124. .items .badge {  
  125.   display: inline-block;  
  126.   font-size: small;  
  127.   color: white;  
  128.   padding: 0.8em 0.7em 0 0.7em;  
  129.   background-color: #607D8B;  
  130.   line-height: 1em;  
  131.   position: relative;  
  132.   left: -1px;  
  133.   top: -4px;  
  134.   height: 1.8em;  
  135.   margin-right: .8em;  
  136.   border-radius: 4px 0 0 4px;  
  137. }  
  138.   
  139. /* everywhere else */  
  140. * {  
  141.   font-family: Arial, Helvetica, sans-serif;  
  142. }  
  143.   
  144.   
  145. /* 
  146. Copyright 2016 Google Inc. All Rights Reserved. 
  147. Use of this source code is governed by an MIT-style license that 
  148. can be found in the LICENSE file at http://angular.io/license 
  149. */  
Now, create a folder named test and then add TypeScript files given below.

Calculator.ts
  1. import { Component } from '@angular/core';  
  2.   
  3. @Component({  
  4.     selector: 'display-message',  
  5.     template: '<h1>{{result}}</h1>'  
  6. })  
  7.   
  8. export class CalculatorComponent {  
  9.     public result: number = 0;  
  10.   
  11.     constructor() { }  
  12.   
  13.     addNo(a: number, b: number): void {  
  14.         this.result = a + b;  
  15.     }  
  16.   
  17.     clear() {  
  18.         this.result = 0;  
  19.     }  
  20. }  
Message.spec.ts
  1. import { CalculatorComponent } from './calculator';  
  2.   
  3. describe('Testing No state in Calculator component', () => {  
  4.     debugger;  
  5.     let app: CalculatorComponent;  
  6.   
  7.     beforeEach(() => {  
  8.         app = new CalculatorComponent();  
  9.     });  
  10.   
  11.     it('should set new no', () => {  
  12.         app.addNo(10,20);  
  13.         expect(app.result).toBe(30);  
  14.     });  
  15.   
  16.     it('should clear result', () => {  
  17.         app.clear();  
  18.         expect(app.result).toBe(0);  
  19.     });  
  20.   
  21.     it('should clear result', () => {  
  22.         app.clear();  
  23.         expect(app.result).toBe(-1);  
  24.     });  
  25. });  
Now, run the program and output will be, as shown below.