I gave a presentation at work a couple of months ago covering JavaScript scoping considerations for a team of Java developers new to JavaScript.
During the presentation, I used Rhino to run the examples. I have seen other projects that use Rhino for unit testing, but they seemed a little heavy weight for what I wanted to do, which was to write unit tests for the project’s JavaScript and integrate it into our continuous integration environment.
The following explains what I did instead that seems to work well. I’d be interested in hearing any alternative suggestions.
To start with, I used Rhino and env.js to create a more full featured javascript runtime environment in the jvm, as John Resig wrote about a while back. I wrote about this a while back.
Because I wanted to run Rhino and execute JavaScript inside a unit test, it was easiest for me to modify the Shell example provided by the Rhino group.
Note: WordPress was blowing up on me when I tried to show the entire class… this is the abbreviated version with most of my changes.
public class Shell extends ScriptableObject {
private boolean quitting;
private static final long serialVersionUID = -5638074146250193112L;
/**
* Logger for this class.
*/
private static final Logger logger = Logger.getLogger(Shell.class);
@Override
public String getClassName() {
return "global";
}
public static void print(final Context cx, final Scriptable thisObj, final Object[] args, final Function funObj) {
for (int i = 0; i < args.length; i++) {
if (i > 0) {
logger.info(" ");
}
// Convert the arbitrary JavaScript value into a string form.
String s = Context.toString(args[i]);
logger.info(s);
}
}
public void quit() {
quitting = true;
}
public static double version(final Context cx, final Scriptable thisObj,
final Object[] args, final Function funObj) {
double result = cx.getLanguageVersion();
if (args.length > 0) {
double d = Context.toNumber(args[0]);
cx.setLanguageVersion((int) d);
}
return result;
}
public static void load(final Context cx, final Scriptable thisObj,
final Object[] args, final Function funObj) {
Shell shell = (Shell) getTopLevelScope(thisObj);
for (int i = 0; i < args.length; i++) {
shell.processSource(cx, Context.toString(args[i]));
}
}
public static void assertEquals(final String expected, final String actual) {
Assert.assertEquals(expected, actual);
}
public static void assertNotNull(final Object object) {
Assert.assertTrue(!(object instanceof org.mozilla.javascript.Undefined));
Assert.assertTrue("Provided object is null", object != null);
}
public static void assertTrue(final boolean condition) {
Assert.assertTrue(condition);
}
public static void assertFalse(final boolean condition) {
Assert.assertFalse(condition);
}
}
Next I created an Abstract JUnit test that loads my Javascript environment. My setUp method looks something like this:
protected void setUp() throws Exception {
super.setUp();
context = Context.enter();
try {
context.setOptimizationLevel(-1);
shell = new Shell();
context.initStandardObjects(shell);
// Define some global functions particular to the shell. Note
// that these functions are not part of ECMA.
String[] names = { "print", "quit", "version", "load",
"assertEquals", "assertNotNull",
"assertTrue", "assertFalse"};
shell.defineFunctionProperties(names, Shell.class,
ScriptableObject.DONTENUM);
shell.processSource(context, "src/test/resources/scripts/startup.js");
} catch (Exception e) {
e.printStackTrace();
}
}
This initializes the JavaScript context and loads the environment-specific javascript I want to test. In my case I wanted to test code that uses ExtJS. My startup.js script looks like this:
load('src/test/resources/scripts/env.js');
window.location='src/test/resources/scripts/empty.html';
document='src/test/resources/scripts/empty.html';
load('src/main/javascript/extjs/adapter/ext/ext-base.js');
load('src/main/javascript/extjs/ext-all-debug.js');
load('src/test/resources/scripts/resource.js');
load('src/test/resources/scripts/testdata.js');
load('target/myappcode.js');
Order is important here. First, I load env.js. Next I set values for window.location and document. I don't use either explicitly, but some of the other files look for them. Then I load extjs, and my data and app code.
The best part about this arrangement is the predefined functions provided by the shell class. I can easily reference junit assert statements from within my javascript test.
Here is an example JUnit class:
public class PhoneNumberTest extends AbstractJSTestCase {
public PhoneNumberTest(final String name) {
super(name);
}
public void testPhoneCountryCodeRender() {
processScript("src/test/javascript/PhoneCountryCodeRenderTest.js");
}
public void testCreateGrid() {
processScript("src/test/javascript/PhoneNumberCreateGridTest.js");
}
}
and the javascript test code it calls:
var phoneNumber = new edit.form.PhoneNumber(); assertNotNull(phoneNumber); assertNotNull(profile); var grid = phoneNumber.createGrid(profile); assertNotNull(grid); var store = grid.store; assertNotNull(store); assertTrue(store.getCount() === 2); var record1 = store.getAt(0); assertNotNull(record1); var record2 = store.getAt(1); assertNotNull(record2); print(record1.data.phoneNumber); print(record2.data.phoneNumber); print(record1.data.extension); print(record2.data.extension); // Now remove the access code from the phone number record1.data.phoneNumber = PhoneUtils.removeAccessCode(record1); print(record1.data.phoneNumber); record2.data.phoneNumber = PhoneUtils.removeAccessCode(record2); print(record2.data.phoneNumber);
Post a Comment