Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save jamescookie/cbb4cd80dcb776161088 to your computer and use it in GitHub Desktop.
Save jamescookie/cbb4cd80dcb776161088 to your computer and use it in GitHub Desktop.
Unit testing Velocity and JavaScript interactions with Mocha

Unit testing Velocity and JavaScript interactions with Mocha

Here's how we were able to reduce our functional testing and greatly speed up our build times by generating fully rendered [Velocity] (http://velocity.apache.org/) templates on demand and testing their interaction with our JavaScript.

Why bother?

The conventional wisdom is that unit testing views is hard, then therefore best left to functional tests, but these tests tend to be slow and not really designed for testing error scenarios/multiple cases/etc. In our case unit testing the JavaScript without the views would be largly pointless as most of the logic is directly linked to elements on the page. We could have just had some fixture data that emulated our views, but then you get into a maintenance nightmare trying to keep them both in sync. The ideal solution would be to test against the real views.

Why Mocha not Jasmine?

Ordinarily I would have used a Jasmine/Karma combination to test client-side JavaScript, but how do you generate realistic views to test with?

There are, of course, many fine Karma preprocessors that can take your templates and allow you to use them in tests. This is fine when using AngularJS where the whole point is a stable DOM, however, we have a lot of (simple) server-side velocity logic in our views, which would normally mean that it would have to be served by an application somewhere in order to be tested.

This amazing library can interpret velocity templates into a string of HTML. In order to do this it needs to read the files off the file system, which prohibits the use of a real browser as you are unable to see the local file system. Also, it's a node package, which even [browserified] (http://browserify.org/) didn't look like it would work without a lot of effort (let me know if you get it working though - it would have saved a lot of pain).

In order to use this package I wrote the following:

var pageGenerator = function() {
    var PATH = 'some/path';
    var Velocity = require("velocityjs");
    var fs = require('fs');
    var Parser = Velocity.Parser;
    var Compile = Velocity.Compile;
    var currentModel;

    function render(str, macros) {
        var compile = new Compile(Parser.parse(str));
        currentModel = stack.pop();
        stack.push(currentModel);
        var rendered = compile.render(currentModel, macros);
        currentModel = stack.pop();
        return rendered;
    }

    function setCurrentModel(model) {
        stack.push($.extend({}, currentModel, model));
    }

    var readFile = function(fileName) {
        return fs.readFileSync(fileName, 'utf-8').toString();
    };

    var generateFragment = function (which, model) {
        currentModel = {};
        setCurrentModel(model);
        var macros = {};
        macros.parse = function(file) {
            var fileContents = readFile(PATH + file);
            stack.push(currentModel);
            return render(fileContents, macros);
        };

        return render(readFile(PATH + '/' + which), macros);
    };
    
    var generate = function (which, model) {
        var renderContents = generateFragment(which, model);
        return renderContents;
    };

    return {
        generate: generate,
        generateFragment: generateFragment,
        readFile: readFile
    };
};
module.exports = pageGenerator();

Note the use of PATH as our templates are located in a different location from where we are running our tests.

The complicated use of the currentModel stack is necessary due to the way we have multiple nested Velocity macros, each defining a set of parameters to pass to the next level.

Any macros that you use can overridden if required by adding to the macros object, which can allow you to hook into the processing.

Mocha, Node's own testing framework.

[Mocha] (http://mochajs.org/) is geared towards testing server-side JavaScript, and so there are quite a few hoops to jump through in order to get it to play nicely with our newly generated velocity views.

1. Set-up

If you are not familiar with Node/Grunt/Mocha set up, you can find an introduction [here.] (https://gist.github.com/maicki/7781943) Our package.json looks like this:

{
  "devDependencies": {
    "chai": "^1.10.0",
    "chai-string": "^1.1.0",
    "grunt": "0.4.1",
    "grunt-cli": "^0.1.13",
    "grunt-mocha-cli": "~1.11.0",
    "jquery": "^2.1.1",
    "jsdom": "^1.4.1",
    "mocha-jsdom": "^0.2.0",
    "sinon": "^1.12.1",
    "sinon-chai": "^2.6.0",
    "velocityjs": "0.4.6"
  }
}

2. You need a (fake) browser

We used [jsdom] (https://www.npmjs.com/package/jsdom) for this. It fakes a window and document (and a lot more besides) which we can then load our html into. I created a spec-helper class to reduce boilerplate in the tests:

var jsdom = require('mocha-jsdom');
global.pageGenerator = require("./page-generator.js");
global.chai = require('chai');
var vm = require('vm');

before(function () {
    jsdom();
    global.expect = chai.expect;
});

beforeEach(function () {
    global.jQuery = global.$ = require('jquery');
    vm.runInThisContext(pageGenerator.readFile('js/test/lib/chai-jquery.js'));
});

Once this is done, we can then modify the generate method to put our Velocity template into our 'browser':

    var generate = function (which, model) {
        var renderContents = generateFragment(which, model);

        document.head.innerHTML = /<head.*?>([\s\S]*)<\/head>/gm.exec(renderContents)[1];
        document.body.innerHTML = /<body.*?>([\s\S]*)<\/body>/gm.exec(renderContents)[1];
        
        $.ready();
    };

We now have an HTML document that we can generate at will, and maybe that's enough for you to test your page logic, but you probably have a bunch of client-side script that needs testing too.

3. Inject your JavaScript

To inject your JavaScript, you can use the 'vm' package. But first you will probably have to extract it from your page (it won't have been run when we inserted the HTML using document.body.innerHTML), modify the page-generator to do this:

    var vm = require("vm");
    var scriptRegex = /<script .*?src="(.*?)".*script>/g;

    var findScripts = function(text) {
        var jsToLoad = [];
        var match = scriptRegex.exec(text);
        while (match != null) {
            jsToLoad.push(readFile(PATH+match[1]));
            match = scriptRegex.exec(text);
        }
        return jsToLoad;
    };

    var runScripts = function(first, second) {
        var jsToRun = first.concat(second);
        for (var i in jsToRun) {
            try {
                vm.runInThisContext(jsToRun[i]);
            } catch (e) {
                console.log("Failed to load javascript: ", e)
            }
        }
    };
    
    var generate = function (which, model, preBodyScriptsHook) {
        var renderContents = generateFragment(which, model);

        var head = /<head.*?>([\s\S]*)<\/head>/gm.exec(renderContents)[1];
        document.head.innerHTML = head.replace(scriptRegex, "");
        var body = /<body.*?>([\s\S]*)<\/body>/gm.exec(renderContents)[1];
        document.body.innerHTML = body.replace(scriptRegex, "");

        runScripts(findScripts(head), [$('head script').text()]);
        if (preBodyScriptsHook) preBodyScriptsHook();
        runScripts([$('body script').text()], findScripts(body));

        $.ready();
    };

This will run all your JavaScript in the global context so you need to bear in mind that you will need to clean that up between tests (see final section for complete code examples)

4. Stub Ajax calls

Stubbing Ajax can be done with [Sinon] (http://sinonjs.org/), but because of the additional complexities that we have introduced (Mocha/jsdom) this becuase a little more complex.

Firstly, it would be great if we could have used the fakeServer, but that [doesn't work in Node] (sinonjs/sinon#319).

Secondly, we have complicated ajax calls that use multiple facets of ajax (beforeSend, dataFilter, complete, etc), that does not allow us to simply use sinon.stub($, 'ajax').yieldsTo('success', {some: 'data'});

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment