Wednesday, 20 May 2009

RhinoUnit with env.js

In my previous post, I mentioned the possibility of using John Resig's env.js as a means to provide a DOM to RhinoUnit. Since then I've been playing around with it to some success.

As I described before having a DOM available to RhinoUnit would carry a number of advantages. One of the great things about RhinoUnit is that it runs headless, and thanks to the speed of Rhino, tests can run very quickly. It's problem is that it doesn't have a DOM available to it, and so it means that testing view logic is very difficult.

This does have a big advantage that it encourages developers to separate their business logic and view logic, and to have good, loosely coupled testing around their business logic. However it does also mean that the view logic goes basically untested at the unit level. This is less than ideal. It also means that any interactions with frameworks like jQuery and their plugins also cannot be tested by RhinoUnit.

This is where env.js comes in. Env.js is a javascript implementation of the browser, which would allow RhinoUnit to interact with DOM provided they can be made to play nice together. As it turns out, both needed some minor tweaking in order to work with each other.

RhinoUnit uses javascript object self for logging, however env.js overrides this and therefore breaks RhinoUnit's logging. To fix RhinoUnit, self needs to be assigned to a variable and all references to self need to be updated.

Env.js also needs some minor changes. In order to run RhinoUnit in this example, Rhino is hooked into the Bean Scripting Framework for java. This does not provide all the rhino shell commands that env.js expects, specifically the print function. By simply replacing all calls to print with the self instance variable's log function, env.js can be made to work.

Finally, RhinoUnit will fail if it thinks you've polluted the global namespace. This an extremely useful rule, however env.js does rather break that, with the dozens of ways javascript can interact with a browser. As a result RhinoUnit needs to be configured to ignore a rather long list of global variables, including but not limited to:


rhinounit window closed defaultStatus event frames innerHeight innerWidth clientHeight clientWidth name opener outerHeight outerWidth pageXOffest pageYOffset parent screenLeft screenTop screenX screenY status top open close NodeList Node CharacterData Text DOMText CDATASection Comment DocumentType Attr Element DocumentFragment ProcessingInstruction $domparser DOMParser DOMImplementation Document HTMLDocument HTMLElement HTMLCollection HTMLAnchorElement Anchor HTMLAreaElement HTMLBaseElement HTMLQuoteElement HTMLButtonElement HTMLTableColElement HTMLModElement HTMLFieldSetElement Form HTMLFormElement HTMLFrameElement HTMLFrameSetElement HTMLHeadElement HTMLIFrameElement HTMLImageElement HTMLInputElement HTMLLabelElement HTMLLegendElement Link HTMLLinkElement HTMLMapElement HTMLMetaElement HTMLObjectElement HTMLOptGroupElement HTMLOptionElement HTMLParamElement HTMLScriptElement HTMLSelectElement HTMLStyleElement HTMLTableElement HTMLTableCellElement HTMLTableRowElement Event CSS2Properties CSSRule CSSStyleSheet location $currentHistoryIndex $history history navigator setTimeout setInterval clearTimeout clearInterval addEventListener removeEventListener dispatchEvent onerror XMLHttpRequest getComputedStyle screen moveBy moveTo resizeBy resizeTo scroll scrollBy scrollTo alert confirm prompt $profiler $profile document node iRet nodeList


Once you have RhinoUnit and env.js behaving, you can write tests as follows:

eval(loadFile('env-js/dist/env.rhino.js'));
eval(loadFile('example/lib/adminUtils.js'));
eval(loadFile('example/src/TagPickerInputView.js'));

var templateRenderer;
var view;

testCases(test,
function setUp() {
templateRenderer = {};
view = new rhinounit.example.tagpicker.TagPickerInputView(templateRenderer, 'first-tag-picker');
},

function shouldShowLoadMessage() {
document.loadXML('<div id="first-tag-picker">' +
'<form>' +
'<label for="tagPickerInput1">Please select the first tag</label>' +
'<input id="tagPickerInput1" type="text" class="tag-picker-input" name="tagPickerInput" autocomplete="off"/>' +
'<div id="loadMessage" class="tag-picker-load-message hidden">Loading...</div>' +
'<div class="tag-picker-matched-tags"></div>' +
'</form>' +
'</div>');
document.getElementById('loadMessage').style.display = 'none';

view.showLoadMessage();
assert.that(document.getElementById('loadMessage').style.display, eq('block'));
});


Although env.js also apparently supports jQuery, my attempts to integrate the two with RhinoUnit have so far been fruitless. This may be due to the fact that although env.js does offer a high proportion of the features available in your average browser, the holes in functionality are still too great at the time of writing to satisfy the recent versions of jQuery.

Footnote: There are a couple of branches of env.js. This is the one that I used: link.

Friday, 8 May 2009

Screw.Unit outside the browser

Screw.Unit seems to be the current favourite means of testing JavaScript code. It has many advantages. It has nice BDD style syntax. It has a custom matchers. It's easy to extend. It has many lovely things.

It also runs inside the browser, which means it has access to the DOM. This lets you test against the DOM which is handy, but does result in the overhead of having to run a browser for your tests. JavaScript testing frameworks like RhinoUnit don't run in the browser and as a result can run headless and can run a lot faster. The disadvantage is, of course, that these frameworks cannot test against the DOM.

This isn't an altogether bad thing. In order to test under RhinoUnit, one must separate view logic from business logic, forcing the developer to think about good encapsulation. It does however still leave you with the question of how to quickly test javascript against a DOM.

John Resig's Env.js provides one possible alternative. It looks like a solution that would integrate well with RhinoUnit. Another possible solution which I've chosen to explore was one which was originally discussed with Fabio Lessa, a colleague of mine, which is to run Screw.Unit tests from inside HtmlUnit.

The following mainfile combined with the subsequent buildr buildfile generates a jar that can be used to run Screw.Unit test suites. The onejar reference in the buildfile allows me to package all the dependency jars into a single jar with the mainfile. This saves me having to add all the dependency jars to the classpath. It isn't strictly necessary here. (For more details on the subject, see Liz Douglass' post)

Mainfile:
package net.randomfocus.screwunitrunner;

import java.io.File;

import be.roam.hue.doj.Doj;

import com.gargoylesoftware.htmlunit.WebClient;
import com.gargoylesoftware.htmlunit.html.HtmlPage;

public class ScrewUnitRunner {

public static void main(String[] args) throws Exception {
for (String arg : args) {
runTestsForSuite(new File(arg).toURL().toString());
}
System.out.println("Screw.Unit tests passed.");
}

private static void runTestsForSuite(String suite) throws Exception {
WebClient client = new WebClient();
HtmlPage page;
page = (HtmlPage) client.getPage(suite);
Doj dom = Doj.on(page);

// Allow tests to run.
Integer timeout = 0;
while ("Running...".equals(dom.get(".status").text())) {
Thread.sleep(100);
timeout++;
if (timeout > 300) {
throw new RuntimeException("Test in suite [" + suite
+ "] timed out.");
}
}

// Check for failures
if (dom.get(".error").allElements().length > 0) {
throw new RuntimeException("Test in suite [" + suite + "] failed.");
}
}
}


Buildfile:
require 'buildr'

VERSION_NUMBER = '1.0'

HTMLUNIT_JARS = ['xalan:xalan:jar:2.7.1',
'net.sourceforge.htmlunit:htmlunit:jar:2.5',
'net.sourceforge.htmlunit:htmlunit-core-js:jar:2.5',
'net.sourceforge.cssparser:cssparser:jar:0.9.5',
'commons-httpclient:commons-httpclient:jar:3.1',
'commons-io:commons-io:jar:1.4',
'commons-logging:commons-logging:jar:1.1.1',
'commons-lang:commons-lang:jar:2.4',
'commons-codec:commons-codec:jar:1.3',
'xerces:xerces:jar:2.4.0',
'rhino:js:jar:1.7R1',
'net.sourceforge.nekohtml:nekohtml:jar:1.9.12',
'commons-collections:commons-collections:jar:3.2.1',
'be.roam.hue:hue:jar:1.1',
'org.w3c.css:sac:jar:1.3']

TEST_JARS = ['junit:junit:jar:4.5', 'org.hamcrest:hamcrest-all:jar:1.1']

repositories.local = './repository'
repositories.remote << 'http://www.ibiblio.org/maven2/'
repositories.remote << 'http://hue.googlecode.com/svn/maven2/'

desc 'Browser Unit'
define 'browserunit' do

project.version = VERSION_NUMBER
project.group = 'scott'

define 'browser' do
compile.with HTMLUNIT_JARS
test.with TEST_JARS
package :jar
end

define 'packaged' do
package(:jar).path("lib").tap do |p|
HTMLUNIT_JARS.each do |j|
p.include artifact(j).to_s
end
end
package(:jar).path("main").tap do |p|
p.include project('browser').package(:jar)
end
package(:jar).with :manifest=>manifest.merge(
'One-Jar-Main-Class'=>'net.randomfocus.screwunitrunner.ScrewUnitRunner',
'Main-Class'=>'com.simontuffs.onejar.Boot'
)
end

end


Running it:
buildr clean package
java -jar packaged/target/browserunit-packaged-1.0.jar screwunit/screw-unit/example/spec/suite.html


Now if we were to try running those against real suites, we'd see that the error reporting is currently nonexistent. Those nice Screw.Unit test reports are lost. Finding a way to represent those accurately is the next big step for making this a viable solution for JavaScript testing outside of the browser.