10 July 2013

Jasmine - Rhino - Ant

Ant on JasmineSome time ago I decided to brush up my poor JavaScript skills. I did not want to use JavaScript to develop web applications but was interested in the language itself. Naturally I started with code katas and little assignments like the ones found at Ruby Quiz. I needed a unit testing framework but during my first steps did not feel like looking for a suitable library and just rolled my own Assert class on top of Rhino which was, at this time, sufficient for me to get things done.

Later when I wanted regular testing during continuous integration, I looked for unit testing frameworks and found Tiest Vilee's RhinoUnit which did the job well. It felt much like JUnit, as implied by its name, and I had no problems using it. After some time I noticed Jasmine being referenced in several presentations and set out to try it. It is a behaviour-driven development framework for testing JavaScript code and does not depend on any other frameworks.

Jasmine and Rhino
Others have successfully ran Jasmine with Rhino using Envjs. (Here is a more detailed description how to do that.) Their intention was to test JavaScript used in web pages without launching a browser by using the simulated browser environment provided by Envjs. As I did not work in a browser environment this approach seemed wrong and I looked for alternatives. David Green used Jasmine on Rhino in his Rescripter, just providing missing global definitions and I worked on top of his code:
(function(global) {

    // timer functions from global for "jasmine.Clock"
    var timer = new java.util.Timer();
    var counter = 1;
    var ids = {};

    global.setTimeout = function(fn, delay) {
        var id = counter;
        counter += 1;
        ids[id] = new JavaAdapter(java.util.TimerTask, { run : fn });
        timer.schedule(ids[id], delay);
        return id;
    };

    ...

})(this);

Standalone Rhino
The version of Rhino shipped with the JDK has been modified by Sun. For example the JavaAdapter has been removed, as written in this document by Oracle. As jasmine-rhino.js (listed above) needs the JavaAdapter to implement a TimerTask, I used the standalone Rhino. To invoke it from javax.script, there must be a ScriptEngineFactory but the standalone Rhino does not provide any. Fortunately I found com.sun.phobos.script.javascript.RhinoScriptEngineFactory on java.net.

Jasmine Reporters
Jasmine uses reporters to report test failures in different formats. Larry Myers offers several useful Jasmine Reporters, e.g. jasmine.console_reporter.js or jasmine.junit_reporter.js which creates XML files suitable for JUnit report generation. These reporters expect an Envjs console and I found myself turning back to Envjs after all. But in the end I created my own console that delegated to the Ant task logger.
Console = function() {
    var logger = {

        error : function(message) {
            self.log(message, 0);
        },

        warn : function(message) {
            self.log(message, 1);
        },

        ...
    };

    return logger;
};
this.console = new Console();
To finally run Jasmine I created a spec runner (AntSpecRunner.js) derived from Jasmine's original SpecRunner.html which started the tests and waited for all of them to finish.
this.execJasmine = function() {
    var jasmineEnv, apiReporter;

    jasmineEnv = jasmine.getEnv();
    jasmineEnv.updateInterval = 100;

    apiReporter = new jasmine.JsApiReporter();
    jasmineEnv.addReporter(apiReporter);

    jasmineEnv.execute();

    while (!apiReporter.finished) {
        java.lang.Thread.sleep(100);
    }
};

console.log('Jasmine loaded.');

Rhino and Ant
For continuous integration I added Ant support. Now Ant is not the newest nor hottest technology available but I know my way around. Inspired by RhinoUnit I load Jasmine code to execute in Rhino using Ant's Scriptdef task. The script implementation iterates all files of all provided FileSets and calls runTest with each file. Note that self points to Ant's ScriptDefBase task implementation by default.
var testfailed = false;

// helper methods and Ant attributes omitted

var filesets = elements.get("fileset");
for (var j = 0; j < filesets.size(); j += 1) {
    var fileset = filesets.get(j);
    var directoryScanner = fileset.getDirectoryScanner(project);
    var srcFiles = directoryScanner.getIncludedFiles();

    forEachElementOf(srcFiles, function(srcFile) {
        var jsfile = new java.io.File(fileset.getDir(project), srcFile);
        runTest(jsfile);
        if (testfailed && haltOnFirstFailure) {
            self.fail("Jasmine failed.");
        }
    });
}

if (testfailed) {
    self.fail("Jasmine failed.");
}
Each source file, supposed to be a Jasmine spec, is loaded and executed. I created a special Jasmine reporter, jasmine.CountsReporter, to get the number of failed tests back from Jasmine to record failures.
function runTest(file) {
    var failingTests;

    // helper methods omitted

    try {
        Load.file(file);
    } catch (e) {
        erroringTestMessage(file, e);
        testfailed = true;
    }

    failingTests = this.jasmine.getEnv().countsReporter.failingTestNames();
    if (failingTests.length > 0) {
        forEachElementOf(failingTests, function(testName) {
            failingTestMessage(testName);
        });
        testfailed = true;
    }
}
(See the complete integration working here.)

Although I was reusing a lot from rhinoUnitAnt.js and copying from David and others, to integrate Jasmine, Rhino and Ant took me much longer than I had expected. While this might be a general statement about any coding task, I am wondering if I should have went with one of the existing solutions...

2 comments:

David Bosschaert said...

Thanks for this great post, it was really useful to me. I took this stuff and got it working under Maven too. Blogged about it here.
Thanks again, Code Cop!

Peter Kofler said...

Thank you for letting me know. I am happy this is useful for someone at least ;-)