Browserify libraries that can be tested with Jasmine... before they are rolled up by Browserify

This is part of a series of notes about bringing a JS project, sf-active-js, over to a contemporary Javascript development environment.

This is a trick to write modules that are testable in a browser, without subjecting the test to Browserify. Why?

I want to develop without the compilation step. It's easier and faster to work in the browser. (This particular app is mostly DOM manipulation, so headless isn't important.) Once the module has settled down, it can be added to the library.

The rest of this note assumes you know the Jasmine sample test runner included with the distro.

The problem is, for Browserify, we have "require()" and "module.exports =" in the source. There isn't a JS library that makes this work in the browser, with that exact syntax.

So, what I did was write a fake library to neutralize those functions. Load this right before your libraries:
jasmine/require.js:

require = function(name) {
    console.log('fake require called with ' + name);
};
module = {
    exports: {}
};

Now, calls to require won't do anything. This means that you have to manually add script tags to your SpecRunner.html file to load your libraries in the right order.

Our spec is called test-me.js.

The test we run looks like this:

describe("test-me", function() {
    it("should add one to the value.", function() {
        var testMe = new TestMe(1);
        expect(testMe.add(1)).toEqual(2);
    });
});

And the module test-me.js is:

function TestMe(x) {
    var y = x;
    function add(z) {
        return y + z;
    }
    return {
        add: add
    };
}

module.exports = TestMe;

The trick is that we're defining TestMe, a constructor function. All use of this library requires us to create an instance of TestMe.

So, when we load this library via a script tag, it defines TestMe globally.

The module.exports line then causes the constructor function to be exported by Browserify. In the test environment, this line does nothing.

Looking at the test code again:

describe("test-me", function() {
    it("should add one to the value.", function() {
        var testMe = new TestMe(1);
        expect(testMe.add(1)).toEqual(2);
    });
});

That we create testMe first, using the TestMe function that is in the global scope, then use it.

D3 style code

The D3 library uses a simpler style of module. You just manage a global namespace. (I think it's OK - if you partition the namespace, I don't see problems, generally.) D3 puts its code in window.d3 or the global d3 namespace. I'm putting my code into the IMC namespace.

To make things easy, you need an init script to set up the namespace. Add this to the require.js script:

window.IMC = window.IMC || {};

That initializes the namespace.

Subsequently, in src/js/test-me.js you can define this:

IMC.testMe = {
    add: function(z) {
        return z + 1;
    }
};

And then in spec/test-me.js:

    it("should also add one to the value.", function() {
        expect(IMC.testMe.add(1)).toEqual(2);
    });

Why is the d3 style easier?

Testing the d3-style code is a lot easier because we're always writing code relative to the global namespace.

The Browserify environment allows you to write code as if you're operating in a global namespace, but you're really in a local namespace that may not be referenced from the global namespace. (Read about the self-executing anonymous revealing-module pattern.) The challenge is to allow Jasmine to access such a module from the global namespace.

With d3-style code, you're always dealing with the global namespace.

Another way to look at it: purity versus practicality

I'm not sure why, but because JS allows you to execute some code without adding any names to the global namespace, it's become a feature of module writing. Commonjs and Browserify allow that.

(Maybe it's because prototype.js used $, and then jQuery used $. There was a big name clash.)

D3-style coding doesn't do that at all. You put one name into the global namespace, and then hang all your names from that. It's not pure, but it's also not bad. You control your own namespace. The odds of a clash are low.