Best practices for unit testing

This workflow demonstrates the best way to create and run unit tests for Web AppBuilder. Widgets and utilities of Web AppBuilder are all written as Asynchronous Module Definition (AMD) modules. For this reason, this workflow uses Intern as the testing framework, since it supports the Dojo loader.

Note:

This workflow is based on Intern 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 the 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. 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.
    Note:
    If you receive a message saying module cannot be found. The solution is to use the default dojo loader provided by the intern. Use npm install dojo --save-dev to install the dojo module. Open the intern.json configuration file and modify the script value to dojo.
    "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 configuration file is intern.json in the project root. Since many components require DOM to run, it is preferable to run unit tests using Chrome WebDriver, managed by Selenium. Intern defaults to using the Selenium tunnel, making configuration efficient.

  1. Add the 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 Headless Chrome in the same way as 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 configuration file.
    "environments": [{
      "browserName": "chrome",
      "chromeOptions": {
        "args": ["headless", "disable-gpu"]
      }
    }]
  3. For the final step, you need to configure the custom loader that includes Dojo and custom packages. After that, the configuration file looks like the following:
    {
      "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

Organize the files for unit testing as detailed below.

Widgets and themes

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

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

There is a file named all_tests.js that includes all tests of widgets. The content of the all.js file is as follows:

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

Jimu

Unit tests for jimu are placed in 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 the suites property in the intern.json configuration for all tests to be included (widgets, jimu, and builder).
    {
      "suites": ["tests/unit/all"],
      ...

Write tests

Follow these steps to write a unit test:

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

    See lines 1 and 2 in the code example below.

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

    See lines 3 through 7 in the code example below.

  3. Write the unit test.

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

    A simple test is shown below. For more information on how to write tests, refer to the Writing tests page 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);
        }
      });
    });

Run tests

Complete the following 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 the .js suffix when you run a single suite.

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

Reporter

The Web AppBuilder team uses Jenkins as its continuous integration solution. To integrate with Jenkins, use the junit reporter and enable the Publish JUnit test result report postbuild action for the best display of the test results:

npm test reporters=junit > tests/junitReport.xml

Your browser is no longer supported. Please upgrade your browser for the best experience. See our browser deprecation post for more details.