2011-08-28

How to write and unit-test universal JavaScript modules (browser, Node.js)

Update 2011-11-19. This post is now mostly superseded by “Bridging the module gap between Node.js and browsers”.

Node.js has a very nice module system that is easy to understand, yet distinguishes between the exports of a module and things that should be private to it. This post explains how the code of a Node.js module can be modified so that it works on both Node.js and web browsers. It also explains how to unit-test such code.

1. Writing a universal module

1.1. Node.js modules

A Node.js module looks as follows:
    var privateVariable = 123;
    function privateFunction() {
    }
    
    exports.publicVariable = 345;
    exports.publicFunction = function () {
        return privateVariable * exports.publicVariable;
    }
You add your public data to the object exports which Node.js manages for you. Additionally, Node.js ensures that all other data stays private to your module. Inside the module, you refer to private data directly, to public data via exports. Sect. 2 shows how such a module is used.

1.2. Making Node.js code universal

You can use Node.js module code in browsers, if you wrap it as follows:
    "use strict";

    (function(exports) {
        
        // Your Node.js code goes here

    }(typeof exports === "undefined"
        ? (this.moduleName = {})
        : exports));
Let’s examine the wrapper code:
  • Enabling strict mode: The first line enables strict mode [1] under ECMAScript 5 and does nothing under older versions of JavaScript.
  • An immediately invoked function definition (IIFE): We define a function and immediately invoke it [2]. This serves two purposes:
    • It keeps non-exported data private in browsers, where Node.js does not do this for us.
    • It conditionally creates the variable exports which exists on Node.js, but must be created in a browser. Creating a new scope with a new variable exports in it is the only way of doing this, you cannot add a variable to an existing scope if it isn’t there, yet. That is, you cannot do the following:
          if (typeof exports !== "undefined") {
              var exports = {}; // (*)
          }
      
      Reason: JavaScript is function-scoped – a variable always exists inside the complete (inner-most) enclosing function. Additionally, variable declarations are hoisted – moved to the beginning of the function. Thus, the var declaration (but not the assignment!) at (*) is moved before the if statement and exports will always be undefined when the check is performed. Note that the code would work without hoisting, but not with block-scoping, because then the exports declared at (*) would only exist inside the then-block.
  • typeof exports === "undefined": Does the variable exports exist?
  • this.moduleName = {}: At the global level, this refers to the global object. Therefore, the assignment creates the global variable moduleName (which holds the module in a browser).
Running example:
    "use strict";

    (function(exports) {

        exports.StringSet = function () {
            this.data = {};
            // arguments is not an array; borrow forEach
            Array.prototype.forEach.call(arguments, function(elem) {
                this.add(elem);
            }, this); // pass "this" on to the function
        }

        exports.StringSet.prototype.add = function(elem) {
            if (typeof elem !== "string") {
                throw new TypeError("Argument is not a string: "+elem);
            }
            this.data[elem] = true;
            return this; // allow method chaining
        }

        exports.StringSet.prototype.contains = function(elem) {
            // Comparison ensures boolean result
            return this.data[elem] === true;
        }

    }(typeof exports === "undefined"
        ? (this.strset = {})
        : exports));

2. Using a universal module

Node:
    var strset = require("./strset");
    var s = new strset.StringSet();
Browser:
    <script src="strset.js"></script>
    <script>
        var s = new strset.StringSet();
    </script>

3. Unit-testing a universal module

3.1. Node.js: unit-testing via the module assert

We use Node.js’ built-in means for unit-testing – the module assert.
    var assert = require('assert');
    var strset = require('./strset');

    // constructor
    (function() {
        var sset = new strset.StringSet("a"); 
        assert.ok(sset.contains("a"));
        assert.ok(!sset.contains("b"));
    }());

    // add - illegal arguments
    (function() {
        assert.throws(function() {
            new strset.StringSet().add(3);
        });
    }());
These tests are run by storing them in a file strset-node-test.js and executing it via the command line:
    > node strset-node-test.js
If one of the tests fails, there will be an exception. If all tests succeed, nothing happens.

3.2. Browser: unit-testing via jQuery’s QUnit

In a browser, we have (too) many options: Most frameworks come with their own unit-testing support. Let us use jQuery’s QUnit.
<!doctype html>
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
        <title>strset-test</title>
        <script src="http://code.jquery.com/jquery-latest.js"></script>
        <link rel="stylesheet" href="http://code.jquery.com/qunit/git/qunit.css" type="text/css" media="screen">
        <script type="text/javascript" src="http://code.jquery.com/qunit/git/qunit.js"></script>
        <script type="text/javascript" src="strset.js"></script>
        <script>
            $(document).ready(function(){
                test("constructor", function() {
                    var sset = new strset.StringSet("a"); 
                    ok(sset.contains("a"));
                    ok(!sset.contains("b"));
                });
                test("add - illegal arguments", function() {
                    raises(function() {
                        new strset.StringSet().add(3);
                    });
                });
            });
        </script>
    </head>
    <body>
        <h1 id="qunit-header">strset-test</h1>
        <h2 id="qunit-banner"></h2>
        <div id="qunit-testrunner-toolbar"></div>
        <h2 id="qunit-userAgent"></h2>
        <ol id="qunit-tests"></ol>
        <div id="qunit-fixture">test markup, will be hidden</div>
    </body>
</html>
You execute these tests by saving them in an .html file and opening it in a browser.

4. Related reading

Related reading
  1. JavaScript’s strict mode: a summary
  2. JavaScript variable scoping and its pitfalls
  3. Modules and namespaces in JavaScript
  4. pattern for module targeting browser and nodejs. Inspiration for the above pattern. Does the same thing, but in reverse – one uses the same variable to export public data on both Node.js and browsers. On Node.js that variable points to the exports object, on browsers, it points to an object in a global variable.

6 comments:

David Bruant said...

"typeof exports === "undefined": Does the variable exports exist?"
=> The variable existence is not what you care about.
The information you need for the rest of your code to work is that exports is something you can use when you do "exports.StringSet = function (){...}".
What you want to test for that is not that the variable exists, but that exports is an object in the ES5.1 - 4.3.3 sense which you can test as:
typeof exports === 'object' || typeof exports === 'function' (I don't know anything shorter)

Axel Rauschmayer said...

Good point! What one is looking for is a test such as "can I add properties to the value 'exports'?". That would be the most "intention-revealing" way of coding this. But there is currently no easy way of performing such a test, so I’m opting for the simpler check and assume that things will fail quickly if there is a name clash.

I’m currently reworking this approach to enable importing of other modules.

Miller Medeiros said...

I've been using a slightly different approach, wrap my code similar to the revealing modules pattern: https://github.com/millermedeiros/crossroads.js/blob/776f603c/dist/crossroads.js#L340-357 - that way it is very easy to add dependencies.

and I'm using jasmine to test my code everywhere (so I can share the same "specs" for node and browser) -  see running unit tests at the end of README file: https://github.com/millermedeiros/crossroads.js/tree/a03c8ef5d

I think it is a bad idea to have 2 different unit tests frameworks for the same project, you need twice the work to make sure everything runs fine on all the environments.

if you prefer to use QUnit , @jdalton created a command line boilerplate to run QUnit on node, rhino and browser ( https://github.com/jdalton/qunit-clib ) - I prefer Jasmine tho, the error messages are clearer and also because since you describe what you expect the code to do you end up thinking about edge cases.

Miller Medeiros said...

I would probably remove the "use strict" from the code samples, it shouldn't be used unless you know what you are doing and how it can affect your code in the future (when browsers starts supporting it properly) - it may break things badly and for "no real gain"...

Axel Rauschmayer said...

Good stuff! The update that I’m currently working on looks similar to your approach. I’ll keep Jasmine in mind.

Axel Rauschmayer said...

You can apply strict mode selectively so that it doesn’t break old code. I find it very helpful for writing new code, because you quickly get errors for several bad practices (that you should avoid, anyway). Go through the list of things that strict mode does – it might not be as radical as you think.

Web Analytics