Mocking AMD Modules with Squire.js

Monday, April 20th, 2015

Writing unit tests when working with Require.js is tough. Out of the box, Require.js does not make mocking very easy. It involves setting up a separate Require.js context and a complicated setup in a beforeEach.

Imagine you have a module called qs (stands for query string) that parses the query string on page load and creates an object containing all query string parameters.

A Query String AMD Module

define([], function() {
  var parse = function(qs) {
    if (qs[0] === '?') {
      qs = qs.substring(1);
    }

    return qs.split('&').reduce(function(prev, str) {
      var pair;
      pair = str.split('=');
      prev[pair[0]] = decodeURIComponent(pair[1]);
      return prev;
    }, {});
  };

  var params = parse(window.location.search);

  return {
    params: params
  };
});

The implementation of parse() doesn’t matter. The thing that makes this module difficult to unit test is that window cannot be mocked. In a unit test, window.location.search doesn’t exist. When I load up this module into a unit test, I can’t really test that qs.params is the parsed query string because window.location.search was an empty string (the default value if no query string exists). So how can we mock out the window object? Squire.js, makes mocking AMD modules extremely simple and intuitive.

Mocking the Window Object

First, let’s wrap window in its own AMD module:

// window.js
define(function() {
  return window;
});

Next, we can specify the AMD module window as a dependency for the qs AMD module:

// qs.js
define(['window'], function(window) {
  var parse = function(qs) {
    if (qs[0] === '?') {
      qs = qs.substring(1);
    }

    return qs.split('&').reduce(function(prev, str) {
      var pair;
      pair = str.split('=');
      prev[pair[0]] = decodeURIComponent(pair[1]);
      return prev;
    }, {});
  };

  var params = parse(window.location.search);

  return {
    params: params
  };
});

Unit Testing with Squire

Once you install Squire, you can simply create an instance of it and use the mock method to specify a mock for a particular AMD module.

define(['Squire'], function(Squire) {
  describe('qs.params', function() {
    var injector;

    beforeEach(function() {
      injector = new Squire();
    });

    afterEach(function() {
      injector.remove();
    });

    it('should contain an objet of all query string params', function(done) {
      injector
        .mock('window', {
          location: {
            search: '?t=veg&color=blue'
          }
        })
        .require(['qs'], function(qs) {
          expect(qs.params).toEqual({
            t: 'veg',
            color: 'blue'
          });

            done();
        });
    });
  });
});

In the example above, I am mocking the window AMD module that I created with a simple JavaScript object containing a static location object. After creating the window mock, I can then chain a call to require to load up the qs module using the window mock instead of the window AMD module (which just returns the real window object). How awesome is that!

Be aware that injector.require() is asynchronous like require(). This example is using Jasmine 2.x which allows you to pass in a done() function to your test and your test will be considered finished when that done() function is called. If I didn’t have the done() call, the test would finish before the expectation is run, making you think that all your tests passed when in reality one of your expectations wasn’t being run.

Squire and Karma

If you are using Require.js with Karma and want to use Squire, you might run into some issues when you simply run new Squire(). I found the solution on Stack Overflow. In short, this is what I did to get it working:

// test-main.js
requirejs.config({
  // Karma serves files from "/base"
  baseUrl: "/base",

  paths: {
    Squire: "path/to/Squire",
  }

  // ask Require.js to load these files (all our tests)
  // deps: tests,

  // start test run, once Require.js is done
  // callback:
});

require(tests, function() {
  window.__karma__.start();
});

Simply comment out the deps and callback keys in the requirejs.config call and place a separate require call after it. I’m not entirely sure what is going on here but if someone knows, please let me know in the comments! =)

Disclaimer: Any viewpoints and opinions expressed in this article are those of David Tang and do not reflect those of my employer or any of my colleagues.

comments powered by Disqus