Skip to content

Headless Javascript Unit Testing

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);
[Slashdot] [Digg] [Reddit] [del.icio.us] [Facebook] [Technorati] [Google] [StumbleUpon]

Post a Comment

Your email is never published nor shared. Required fields are marked *
*
*