Unit testing Ionic applications

July 25, 2017, 9:05 am Categories:

Categories

Unit testing has rapidly grown from being a relatively niche requirement for front-end/mobile developers to an in-demand practice over the last 4 - 5 years.

For those who may be unaware a unit test involves testing the smallest possible piece of an application's code - typically a function or method for example.

Most job roles and contract opportunities that are now posted online list experience with writing unit tests as desirable if not crucial. Whether that necessitates working with Jasmine, Karma, Mocha or Chai - to name but a few of the most widely used testing tools - the requirements for unit testing web/mobile applications is growing to the point where even the most reluctant developer has to have even a passing acquaintance with the process.

Developing such testing skills is all good and well but writing unit tests for Ionic applications has always been something of a convoluted and challenging process (even with various efforts in the wider Ionic community to document and share this process to a larger audience).

The reasons for this are varied but can be boiled down to the following points:

  • Requires a wide variety of node packages to be installed
  • Often difficult to understand/poorly documented configuration requirements
  • Set-up often necessitates many different configuration files (which can be hard to keep track of)
  • No single agreed method of implementing/organising a unit test workflow within an Ionic project

As developers we're still waiting on the promised ability to run unit tests to be baked into a future release of the Ionic Framework.

In the meantime this can leave us sometimes scratching our heads as to what is considered best practice in this area (search through testing related threads on Stack Overflow or any developer/testing forum to see just how polarised opinions can be on the matter of unit tests).

This is why I've always been a little reluctant to seriously invest time into unit testing Ionic applications.....that is until I came across Facebook's Jest testing framework.

I jest you not

Having been, to put it mildly, dissatisfied with efforts from the wider Ionic community to establish unit testing approaches for Ionic applications I was not expecting a great deal from the Jest testing framework.

What I found, as I explored this framework though, completely reversed my expectations:

  • Quick and simplified installation process
  • Minimal configuration requirements
  • Extensive and well documented testing API
  • JavaScript/TypeScript support
  • Intuitive syntax
  • Speedy execution of tests
  • Well supported (Facebook's little baby!)
  • Wider industry adoption (IBM, NY Times, PayPal, eBay, Twitter & Cisco....to name but a few major companies using the framework)

With those kind of credentials how could I not want to use this in my Ionic projects?

Over the course of this tutorial I'm going to take you through how to install, configure and use Jest to run unit tests for your Ionic applications.

What to expect

We'll be creating a basic Ionic project - imaginatively named ionic-unit - to test a Mock service.

We WON'T be practicing TDD (Test Driven Development) principles - which requires that the unit tests we will be writing should fail - as, quite frankly, I'm only interested in demonstrating how Jest can be used with Ionic.

If you're a TDD adherent I'll leave that to you.

As our project's only purpose is to run a suite of unit tests let's start setting the groundwork for this.

Laying the foundations

As always with Ionic we open up a command line console and with the Ionic CLI create a simple project titled ionic-unit that uses a blank template like so:

ionic start ionic-unit blank

Once the project has been generated create the following directories in the ionic-unit/src directory:

  • mocks
  • unit-tests

With these sub-directories in place we'll now move on to installing the following node packages:

  • jest - Facebook's JavaScript testing framework
  • ts-jest - Allows Jest to test projects written in TypeScript
  • reflect-metadata - Allows metadata to be parsed when running unit tests

All of which is accomplished with these commands:

npm install --save-dev jest
npm install --save-dev ts-jest @types/jest
npm install --save-dev reflect-metadata

With the necessary software packages now installed the next step, before we can start writing our unit tests, is to configure the project's package.json and tsconfig.json files.

Setting the environment

In the project's package.json file add the following configuration (after the scripts and before the dependencies sections):

"jest": { 
   "transform": {
      ".(ts|tsx)": "/node_modules/ts-jest/preprocessor.js" 
   },
   "testRegex": "(/__tests__/.*|\\.(test|spec))\\.(ts|tsx|js)$", 
   "moduleFileExtensions": [
      "ts", 
      "tsx", 
      "js",
      "json"
   ]
}

This addition to the project's package.json file simply allows the Jest framework to accomplish the following tasks:

  • Parse TypeScript files
  • Recognise that unit tests are identified by the test or spec keyword contained within their filename
  • The array of file extensions that test modules use

With the Jest configuration object in place the final addition to the package.json file will allow test scripts to be run using Jest.

This is accomplished by adding the following snippet:

"test": "jest"

Within the scripts configuration block like so:

"scripts": {
   "clean": "ionic-app-scripts clean",  
   "build": "ionic-app-scripts build",
   "lint": "ionic-app-scripts lint", 
   "ionic:build": "ionic-app-scripts build", 
   "ionic:serve": "ionic-app-scripts serve", 
   "test": "jest"
}

With the necessary configurations for the package.json file completed we now need to turn to making similar amendments to the project's tsconfig.json file.

Configuring the TypeScript compiler

The project tsconfig.json file contains a number of compiler options that instruct the Ionic CLI on how the project TypeScript code should be transpiled into JavaScript.

We'll need to make the following additions to this file so that project unit tests will run without being transpiled as part of the application source code:

  • Implement the following flag: "skipLibCheck" : true
  • Declare that the src/mocksand src/unit-tests directories are to be excluded from transpiling

These are implemented within the tsconfig.json file like so:

{
  "compilerOptions": {
    "allowSyntheticDefaultImports": true,
    "declaration": false,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "skipLibCheck" : true,
    "lib": [
      "dom",
      "es2015"
    ],
    "module": "es2015",
    "moduleResolution": "node",
    "sourceMap": true,
    "target": "es5"
  },
  "include": [
    "src/**/*.ts"
  ],
  "exclude": [
    "node_modules",
    "src/mocks",
    "src/unit-tests"
  ],
  "compileOnSave": false,
  "atom": {
    "rewriteTsconfig": false
  }
}

Mocking the project

With the necessary packages installed and configured we can now move onto creating a Mock service for the project.

A Mock, in the context of unit testing, is an object that simulates the behaviour of real objects (such as Angular services for example) without relying on the use of external dependencies (such as modules imported into a service for dependency injection).

As a rule of thumb it's good practice to create Mocks for the code that you want to test.

Doing so ensures that the code you are testing does not rely on various module dependencies and allows only the specific units that need to be tested to be focussed upon.

For the purpose of this tutorial we are going to create a Mock service named MockDatesProvider (which will be located within the src/mocks directory that we created earlier) that contains the following date related methods:

  • addLeadingZerosToDateValueIfRequired
  • returnMonthsOfTheYear
  • returnCurrentMonth
  • returnCurrentDate
  • returnCurrentDateAndTime
  • returnCurrentTimestamp

These are detailed in full in the src/mocks/mock.dates.ts Mock service as shown below:

import { Injectable } from '@angular/core'; 


@Injectable()
export class MockDatesProvider 
{


   /**
    * Months of the year
    */
   private _MONTHS : any =  [ "January", 
       	   					  "February", 
       	   					  "March", 
       	   					  "April", 
       	   					  "May", 
       	   					  "June",
       	   					  "July", 
       	   					  "August", 
       	   					  "September", 
       	   					  "October", 
       	   					  "November", 
       	   					  "December" ];

   constructor() 
   {}




   /**
    *
    * Determines whether a date value needs a leading zero to be added to it or not
    *
    * @method addLeadingZerosToDateValueIfRequired
    * @return {String}
    *
    */
   addLeadingZerosToDateValueIfRequired(digit : number) : string
   {
       let num  : any      =   digit;
       if(num < 10)
       {
           num        =   '0' + num;
       }
       return num;
   }




   /**
    *
    * Return all of the months of the year
    *
    * @public
    * @method returnMonthsOfTheYear
    * @return {Array}
    *
    */
   returnMonthsOfTheYear() : any
   {
      return this._MONTHS;
   }




   /**
    *
    * Return the current date (Year, month & day format)
    *
    * @public
    * @method returnCurrentMonth
    * @return {String}
    *
    */
   returnCurrentMonth() : any
   {
       let currDate       : any    =   new Date(),
       	   currMonth      : any    =   this._MONTHS[currDate.getMonth()];
       return currMonth;
   }





   /**
    *
    * Return the current date (Year, month & day format)
    *
    * @public
    * @method returnCurrentDate
    * @return {String}
    *
    */
   returnCurrentDate() : any
   {
       let currDate       : any    =   new Date(),
           currYear       : any    =   currDate.getFullYear(),
           currMonth      : any    =   this.addLeadingZerosToDateValueIfRequired((currDate.getMonth() + 1)),
           currDay        : any    =   this.addLeadingZerosToDateValueIfRequired(currDate.getDate()),
           currDateValue  : any    =   currYear + '-' + currMonth + '-' + currDay;
   
       return currDateValue;
   }




   /**
    *
    * Return the current timestamp
    *
    * @public
    * @method returnCurrentTimestamp
    * @return {Integer}
    *
    */
   returnCurrentTimestamp() : number
   {
       let currentTimestamp : number  =   Math.floor(Date.now()/1000);
       return currentTimestamp;
   }


}

These shouldn't require any further elaboration given the descriptive comments (couched in JSDoc syntax) above each property/method so let's move onto writing the actual unit test for testing the MockDatesProvider Mock service.

Defining the tests

With the MockDatesProvider now in place we can start writing individual tests to determine whether the logic for our methods is sound or not.

As mentioned earlier tests written for Jest must contain either .spec or .test within their file name.

Within the src/unit-tests directory create a new file named dates.test.ts - we'll use this to define a suite of individual unit tests for testing the methods from the MockDatesProvider Mock service.

A typical unit test can be broken down into 3 component parts:

  • A test suite that groups and organises all the unit tests for a particular section of the codebase (in this context our Mock service being tested)
  • Individual unit tests (defined within a test block)
  • The expectations of the test (what the expected outcome should be)

The Jest unit tests for our project then are structured within the src/unit-tests/dates.test.ts file like so:

import 'reflect-metadata';
import { MockDatesProvider } from '../mocks/mock.dates';


/**
 * Block level variable for assigning the Mock DatesProvider service to
 *
 */
let date 				= null;



/**
 * Re-create the MockDatesProvider class object before each 
 * unit test is run
 *
 */
beforeEach(() => {
   date         = new MockDatesProvider();
   
});



/**
 * Group the unit tests for the MockDatesProvider into one
 * test suite 
 *
 */
describe('Dates service', () => 
{
  
   
   /**
    * Test that the returned value matches today's date 
    *
    */
   test('Returns the current date', () =>
   {
      expect.assertions(1);
      let currentDate         = date.returnCurrentDate();

      expect(currentDate).toEqual("2017-07-14");
   });



   /**
    * Test that the total months of the year are returned 
    *
    */
   test('Returns all of the months of the year', () =>
   {
      expect.assertions(2);
      let months              = date.returnMonthsOfTheYear(),
          expected            = ['July', 'November'];

      expect(months).toHaveLength(12);
      expect(months).toEqual(expect.arrayContaining(expected));
   });



   /**
    * Test that the current month is returned
    *
    */
   test('Returns the current month', () =>
   {
      expect.assertions(1);
      let currentMonth        = date.returnCurrentMonth();

      expect(currentMonth).toBe("July");
   });
   

   
   /**
    * Test that the current timestamp is returned
    *
    */ 
   test('Returns the current timestamp', () =>
   {
      expect.assertions(1);
   	let timestamp         = date.returnCurrentTimestamp();
   	expect(timestamp).toBeGreaterThanOrEqual(Math.floor(Date.now()/1000));
   });




});

Given the accompanying comments for each of the above tests the code for the src/unit-tests/dates.test.ts file should be fairly self-explanatory (for further information on what methods are available with the Jest testing framework take a peek at the online documentation).

All that remains now is to actually run these unit tests from our system command line utility.

Open up your CLI of choice, navigate to the root directory of the project and run the following command:

npm test

This will run the Jest node package (and you might be pleasantly surprised at just how quick the software runs in comparison to other test runners), execute the unit tests defined within the src/unit-tests/dates.test.ts file and print their results to the screen like so:

Unit test results for the Jest framework

And that, my readers, is just how simple it is to implement unit testing in an Ionic application!

In summary

Jest makes implementing and running unit tests in Ionic so much simpler and quicker to accomplish than with any other approach that I've come across and experimented with.

The framework's relatively simple configuration combined with a rich testing API and extensive online documentation make this, until the team at Ionic bake unit testing into Ionic by default, my only choice of software for unit testing an Ionic application.

Whether you're relatively new to unit testing or an experienced hand I'd recommend giving Jest a try - you might be pleasantly surprised at its speed, simplicity and the rich collection of API methods.

I'll cover unit testing in further tutorials as there's definitely more that we could build on and take a lot further than what was touched on here.

Let me know what your thoughts and reactions are to this tutorial by leaving your comments in the form below.

If you enjoyed what you've read here then please consider signing up to my mailing list and, if you haven't done so already, take a look at my e-book - Mastering Ionic : Building a Real World Application for further information about working with unit tests in Ionic.

Tags

Categories

Post a comment

All comments are welcome and the rules are simple - be nice and do NOT engage in trolling, spamming, abusiveness or illegal behaviour. If you fail to observe these rules you will be permanently banned from being able to comment.

Top