Best practices for unit testing

Go through this walkthrough to learn the best way to create and run unit tests for Web AppBuilder. Widgets and utilities of Web AppBuilder are all written as AMD(Asynchronous Module Definition) modules. For this reason, we choose Intern as our testing framework which supports dojo loader.

Note:

This workflow is based on Intern is 4.1.5.

Use the following command to install intern: npm install intern --save-dev.

Configure intern

Once you have installed intern 4, you can run intern command under the project root using node_modules\.bin\intern.

Tip:

It's highly recommended to add a script in package.json:

"scripts": {
    "test": "intern",
    ...
Then you can run your unit tests by npm test.

dojoLoader.js

Intern includes scripts for the Dojo 1, Dojo 2, and SystemJS loaders. You want to use the dojo loader included in ArcGIS API for JavaScript as the custom loader.

  1. Create a custom loader under the tests directory with the name dojoLoader.js.

    The code is copied from node_modules/intern/loaders/dojo.js.

  2. Change the dojo.js url from node_modules/dojo/dojo.js to client/stemapp/arcgis-js-api/dojo/dojo.js.
    "use strict";
    intern.registerLoader(function (options) {
      var globalObj = typeof window !== 'undefined' ? window : global;
      options.baseUrl = options.baseUrl || intern.config.basePath;
      if (!('async' in options)) {
          options.async = true;
      }
      intern.log('Configuring Dojo loader with:', options);
      globalObj.dojoConfig = options;
      return intern.loadScript('client/stemapp/arcgis-js-api/dojo/dojo.js').then(function () {
        var require = globalObj.require;
        intern.log('Using Dojo loader');
        return function (modules) {
          var handle;
          return new Promise(function (resolve, reject) {
            handle = require.on('error', function (error) {
              intern.emit('error', error);
              reject(new Error("Dojo loader error: " + error.message));
            });
            intern.log('Loading modules:', modules);
            require(modules, function () {
              resolve();
            });
          }).then(function () {
            handle.remove();
          }, function (error) {
            handle && handle.remove();
            throw error;
          });
        };
      });
    });

intern.json

Intern is configured using a declarative JSON file. The default location for the config file is intern.json in the project root. Since a lot of components require DOM to run, we prefer to run unit tests using chrome WebDriver, managed by Selenium. Intern defaults to using the selenium tunnel making configuration simple.

  1. Add tunnelOptions property:
    {
      "tunnelOptions": {
        "drivers": ["chrome"]
      }
    }

    For unit tests, there is no need to open a browser window to execute testing code. Intern can interact with a headless Chrome in the same fashion as a regular Chrome.

  2. Use the chromedriver to open a headless session by providing headless and disable-gpu arguments to chromedriver in an environment descriptor in the test config.
    "environments": [{
      "browserName": "chrome",
      "chromeOptions": {
        "args": ["headless", "disable-gpu"]
      }
    }]
  3. For the final step, you need to configure the custom loader which includes dojo and custom packages. After that, the configuration file looks like:
    {
      "suites": ["tests/unit/all"],
      "tunnelOptions": {
        "drivers": ["chrome"]
      },
      "loader": {
        "script": "tests/dojoLoader.js",
        "options": {
          "async": true,
          "tlmSiblingOfDojo": false,
          "has": {
            "extend-esri": 1
          },
          "packages": [{
            "name": "dojo",
            "location": "client/stemapp/arcgis-js-api/dojo"
          },{
            "name": "dijit",
            "location": "client/stemapp/arcgis-js-api/dijit"
          }, {
            "name": "dojox",
            "location": "client/stemapp/arcgis-js-api/dojox"
          }, {
            "name": "put-selector",
            "location": "client/stemapp/arcgis-js-api/put-selector"
          }, {
            "name": "xstyle",
            "location": "client/stemapp/arcgis-js-api/xstyle"
          }, {
            "name": "dgrid",
            "location": "client/stemapp/arcgis-js-api/dgrid"
          }, {
            "name": "moment",
            "location": "client/stemapp/arcgis-js-api/moment"
          }, {
            "name": "esri",
            "location": "client/stemapp/arcgis-js-api/esri"
          }, {
            "name": "jimu",
            "location": "client/stemapp/jimu.js"
          }, {
            "name": "themes",
            "location": "client/stemapp/themes"
          }, {
            "name": "libs",
            "location": "client/stemapp/libs"
          }, {
            "name": "dynamic-modules",
            "location": "client/stemapp/dynamic-modules"
          }, {
            "name": "builder",
            "location": "client/builder"
          }, {
            "name": "stemapp",
            "location": "client/stemapp"
          }, {
            "name": "widgets",
            "location": "client/stemapp/widgets"
          }, {
            "name": "sinon",
            "location": "node_modules/sinon/pkg",
            "main": "sinon"
          }, {
            "name": "tests",
            "location": "tests"
          }]
        }
      },
      "environments": [{
        "browserName": "chrome",
        "fixSessionCapabilities": "no-detect",
        "chromeOptions": {
          "args": ["headless", "disable-gpu"]
        }
      }]
    }

File structure

Follow this organization of the files for unit testing.

Widget and Theme

When testing for each widget and theme, double check that the tests directory are placed in the same folder as other resources. Each widget's tests directory should contain a file named all.js which includes all test suites for this widget.

define([
  './utils',
  './ComponentA'
], function() {});

There is a file with name all_tests.js which includes all tests of widgets. The content of this file is:

define([
  './Analysis/tests/all',
  './Infographic/tests/all'
], function() {});

Jimu

Unit tests for jimu are placed: tests/unit/client/jimu.

All unit tests

You can find all unit tests in: tests/unit/client/jimu.

define([
	'../../client/builder/tests/all',
	'../../client/stemapp/widgets/all_tests',
	'./client/jimu/all'
], function() {});

  1. Add the following file to thesuites property in intern.json configuration, in order for all tests to be included (widgets, jimu, and builder).
    {
      "suites": ["tests/unit/all"],
      ...

Writing tests

Here's how to write a unit test:

  1. Get the assertion instance and test interface from the global variable intern.

    See line 1-2 in the below code example.

  2. Run your unit test with your own module. Use define syntax as you write your own module.

    See line 3-7 in the below code example.

  3. Write unit test.

    The Web AppBuilder team uses Object, which is the default interface of intern to write tests. The Web AppBuilder team also uses assert style of chai as the assertion library.

    Below is a simple test. For more information of how to write tests, please refer to the Writing tests from the intern website.

    //1. Get the assertion instance and test interface from the global variable `intern`.
    var assert = intern.getPlugin('chai').assert;
    var registerSuite = intern.getInterface('object').registerSuite;
    
    
    define([
      // 2. Require necessary modules to run your unit test. Use `define` syntax as you write your own module.
      'dojo/_base/config',
      'jimu/utils', 
      './globals'
    ], function(dojoConfig, utils) {
      //3. Write unit test.
      registerSuite('test-jimu-utils', {
        'testReplace1': function() {
          var o1 = {
              a: 1,
              b: 2
            },
            p = {},
            o2;
          o2 = utils.replacePlaceHolder(o1, p);
          assert.deepEqual(o1, o2);
        }
      });
    });

Running Tests

Follow theses steps to run your tests.

  1. Run all unit tests: npm tests.
  2. Run specified unit tests npm test suites=tests/unit/client/jimu/test-utils
    Note:

    Don't include .js suffix when you run a single suite.

    You can provide multiple suites parameters to run several test suites using one command.

Reporter

The Web AppBuilder team uses Jenkins as our continuous integration solution. In order to integrate with Jenkins, use the junit reporter and enable the Publish JUnit test result report post-build action for the best test results display.

npm test reporters=junit > tests/junitReport.xml