Unit testing Promises in Ionic

October 9, 2017, 9:00 pm Categories:

Categories

I covered unit testing with Jest in a previous tutorial so if you need an introduction to working with that testing framework read that tutorial first before proceeding with this one.

Unit testing Ionic applications has been, to put it mildly, challenging to implement as there has been no single, unified approach that developers could follow. Unfortunately the team at Ionic have yet to implement unit testing as a baked-in feature of the framework at the time of writing this tutorial.

Once such a feature is, by default, enabled (or offered) inside the framework then it will be interesting to see how that compares with using Jest as a unit testing framework.

What to expect

We'll be creating a simple Ionic application that uses the Jest testing framework to unit test a single Mock service method that returns a Promise.

Nothing too difficult or exciting - just a proof-of-concept demonstration.

Laying the foundations

Begin by opening up a command line console, navigate to where you want the application to be created and with the Ionic CLI create a simple project titled ionic-testa that uses a blank template like so:

ionic start ionic-testa blank

Once the project has been generated create the following directories in the ionic-testa/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)": "<rootDir>/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 AsyncMockProvider (which will be located within the ionic-testa/src/mocks directory that we created earlier) that contains the following method:

  • fetchByPromise

The full code for the ionic-testa/src/mocks/mock.async.ts Mock service is shown below:

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


export class AsyncMockProvider {
	
   constructor()
   { }



   /**
    *
    * @public
    * @method fetchByPromise
    * @param val {Number}      A numeric value used for data filtering
    * @return {Promise}
    */
   fetchByPromise(val : number) : Promise
   {
      return new Promise((resolve, reject) =>
      {
         let outcome : string;

         if(val == 1)
         {
            outcome = "First";
         }
         else
         {
            outcome = "second";
         }
         resolve(outcome);
      });
   }


}

How Jest handles promises

Jest is surprisingly versatile and intuitive, particularly when it comes to providing methods for unit testing promises.

We'll be exploring the following approaches over the remainder of this tutorial:

  • Returning a promise
  • .resolves matcher
  • Async/Await

As the Jest documentation states none of these approaches is considered superior or preferable to the other so whichever one is chosen is purely a matter of developer preference.

Let's see how each of these approaches might be used to test the above method for handling values from a returned promise before we write the actual unit test itself.

1. Returning a promise

One of the things I really like about Jest is that we can simply return a promise and let that wait to resolve itself.

As with code that we would write in our components/services we simply use the .then() method to handle the return of the asychronous data from the promise.

The following code snippet (taken from the unit test we'll soon be writing) demonstrates how this works:

test('Returns a Promise', () =>
{
   expect.assertions(1);

   return synced.fetchByPromise(1).then((data) =>
   {
      expect(data).toBe('First');
   });
});

DON'T omit the return statement as the promise MUST be returned otherwise the test will complete before the method being tested completes.

2. The .resolves matcher

The .resolves matcher simply wait for the promise to resolve.

Similar to the above approach the return statement must be used to prevent the test completing before the method being tested is completed.

test('Returns a Promise', () =>
{
   expect.assertions(1);
   return expect(synced.fetchByPromise(1)).toPromise().resolves.toBe('First');
});

If the promise is rejected the test automatically fails.

If a test is expected to fail then the .rejects matcher can be used instead:

test('the fetch fails with an error', () => {
  expect.assertions(1);
  return expect(fetchByPromise()).rejects.toMatch('error');
});

It's important to note that with the .rejects matcher that should the promise be fulfilled then the test will automatically fail.

3. Async/Await

Asynchronous tests can also be written with the async keyword in front of the function that is passed to the test and the await keyword within the body of the test like so:

test('Returns a Promise', async () =>
{
   expect.assertions(1);
   const str = await synced.fetchByPromise(1);
   expect(str).toBe('First');
});

This can also be rewritten to include the .resolves matcher with the following syntax:

test('Returns a Promise', async () =>
{
   expect.assertions(1);
   await expect(synced.fetchByPromise(1)).resolves.toBe('First');
});

As you can see when it comes to writing unit tests jest is quite flexible.

Defining the tests

With the AsyncMockProvider now in place and an understanding of the ways in which promises can be tested with Jest we can start writing our unit test.

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

Within the ionic-testa/src/unit-tests directory create a new file named async.test.ts - we'll use this to define an individual unit test for the fetchByPromise method from the AsyncMockProvider Mock service.

As stated in the previous tutorial 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 individual unit test for our project then is structured within the ionic-testa/src/unit-tests/async.test.ts file like so (note that I have provided ALL of the different ways in which promises can be handled by Jest - simply comment out/un-comment these approaches at your own leisure):

import 'reflect-metadata';
import { AsyncMockProvider } from '../mocks/mock.async';



describe('Asynchronous service calls', () =>
{
   let synced : AsyncMockProvider;
    

   beforeEach(() => 
   {
      synced = new AsyncMockProvider();
   });




   test('Returns a promise', async () =>
   {
      expect.assertions(1);


      // 1. Write the promise this way
      /*return synced.fetchByPromise(1).then((data) =>
      {
         expect(data).toBe('First');
      });*/


      // 2. Handle the promise with .resolves method instead
      //return expect(synced.fetchByPromise(1)).toPromise().resolves.toBe('First');


      // 3. Handle the promise using the await keyword
      //const str = await synced.fetchByPromise(1);
      //expect(str).toBe('First');


      // 4. Example #3 can be rewritten like so
      await expect(synced.fetchByPromise(1)).resolves.toBe('First');

   });

});

Given the accompanying comments for the different approaches to handling the same test the code for the ionic-testa/src/unit-tests/async.test.tsfile 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 this unit test 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 test defined within the ionic-testa/src/unit-tests/async.test.ts file and print their results to the screen like so:

As you can see unit testing promises is incredibly easy with Jest (unlike other unit testing approaches for Ionic that I have come across).

In summary

Jest makes unit testing promises in Ionic so much simpler and quicker to accomplish than with any of the other unit testing approaches I've witnessed to date.

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 (whenever that promised functionality will finally be delivered), 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.

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