follow me on twitter

follow me on twitter.

Thursday, February 2, 2012

Leveraging node.js for unit testing in JavaScript development

I played around with node.js to find a way to properly unit test my JavaScripts. While doing that I stumbled upon a nice feature node.js offers by the vm module. The function

vm.runInNewContext(script, globalObject, filename);

allows to run a script and pass a context to it. This context is nothing else than the evil global object of JavaScript we all know about and work around. So the above code allows us to run a script like the one below which is stored in test.js and has two dependencies to the global object:

(function () {
    "use strict";
   
    var renderer = require("./renderer");
   
    assert.ok(renderer);
}());

By using runInNewContext we can now load the test.js script and run it with an empty context. This is done by test-runner.js:


(function () {
    "use strict";
    var vm = require("vm"), fs = require("fs"),
        filename = "app/test.js",
        testScript = fs.readFileSync(filename).toString(),
        globalObject = {};

    vm.runInNewContext(testScript, globalObject, filename);
}());

Now we can run the test runner with node:

#  node test-runner.js

To get an error saying:

ReferenceError: require is not defined

This is exactly what we expected because the globalObject passed to runInNewContext is empty and hence the context has no require to offer as we are used to in a node.js environment. However, being able to pass our own global object, we can mock dependencies the test requires. This is exactly what we need to mock away our collaboraters and unit test our modules in an isolated manner. In Java, libraries like EasyMock do some magic to provide mocks for interfaces and classes. A great and very useful tool for test driven development in Java. In JavaScript mocking a function is a snap compared to that. The dynamic type system and the functional approach with late binding makes it easy to mock a function or an entire object with literals. Hence the test-runner.js script needs to setup the globalObject properly to provide an appropriate global object on which test.js can rely on:

(function () {
    "use strict";
    var vm = require("vm"), fs = require("fs"),
        filename = "app/test.js",
        globalObject = {
          require: function(module) {
            return {};
          }

        },
        testScript = fs.readFileSync(filename).toString();

    vm.runInNewContext(testScript, globalObject, filename);
}());

The above script now offers a require function as a property of the global object. It simply returns an empty object which is enough to fulfill our test case. Now our test script fails a liitle bit later because it misses the assert function to check the imported module. In this case we don't want to reimplement the assert function node.js already offers. Instead we just pass the real implementation:

globalObject = {
     assert: require("assert"),
     require: function(module) {
            return {};
     }
}

Running the test runner passes now without complaint, because we successfully mocked all dependencies for our test case.

The exampled I showed here is very simplicistic. But it clearly shows the opportunity the runInNewContext function of the vm module offers for unit testing JavaScript code. We can not only mock the global require function, but also other global properties as for instance the document or window properties in a browser or a jQuery Ajax call $.ajax({}).

While it realy sounds like a big burden to mock away an entire browser environment, the impact of such an development style on JavaScript code would be huge and raise not only quality and testability but also leads to flexible reusable modules, objects and functions. While I have not applied such an development approach for JavaScript by now, my experiences with TDD in Java makes me sure this would be a good idea for JavaScript as well. Not having Eclipse and its great refactorings for Java (a kindom for Ctrl+1!!) makes it not that easy and powerful, but the benefits make it worth doing it.

As shown above, node.js in general and vm.runInContext in particular offer a nice starting point for such an endeavour. 

4 comments:

  1. I am not (yet) experienced in javascript-testing approaches...
    ... but isn't your approach a bit "reinventing the wheel"?

    At NDC last year I was attending a session by Christian Johansen, where he was TDD-coding in JavScript a whole hour... it seemed like a solved problem.

    The video is here:
    http://ndc2011.macsimum.no/mp4/Day1%20Wednesday/Track5%201620-1720.mp4

    Do you know his book:
    http://tddjs.com/

    I have it, but not read yet.

    ReplyDelete
    Replies
    1. Yepp. It might be the case I'm late. :-)

      I own that book but I'm not finished yet. I just found the vm.runInNewContext function which allows to mock everything at a very low level stage. Obviously the code I've shown above is not at all a testing framework. It's just an approach which allows:

      - to mock everything
      - mock code without 'monkey patching' the browser environement
      - to run a browser JavaScript outside of a browser (and even PhantomJS)

      But I see that I have to hurry up with watching that video (thanks for the link) and reading that testing book (I also bought another JS testing book this week). So give me some time to revoke my blog after getting smarter.

      Thanks for your comment anyway. :-)

      Delete
  2. I would be curious about the other JS testing book :-)

    I hope the "out of browser" testing is getting more an option, as far as I gathered the advice from tddjs is to run the test in the browser and to use a smart framework for this.

    I am looking forward to more insights concerning TDD with JS :-)

    ReplyDelete
  3. So helpful articles for anybody. I like it. I very much enjoyed reading your description of life at sea.
    This blog site is pretty good! How was it made . I view something genuinely interesting about your site so I saved to my bookmarks . George offers Investing in Property opportunities and teaching Commercial to Residential Conversion

    ReplyDelete