Java-based JavaScript unit testing with Rhino

At SwissProt, we test the UniProt website in several ways (though we still lack coverage, which is another topic). One area where testing was lacking for some time was JavaScript, which had accumulated over time to a somewhat substantial amount, about 3200 lines of code. We have Selenium tests for the site as a whole, but having unit tests for individual bits of JavaScript would have been great to make development of new code easier, and increase confidence in the code.

Many available frameworks such as JsUnit do in-browser testing. If you want to test actual UI code, that’s obviously what you need. But we have that at least partly covered with Selenium, and a problem with these tests is that they are difficult to run in an automated way, like from our ant test task. So I wanted something to easily and quickly test the UI-independent code, along with the Java code, from the command line, ant, or scripts. I also think that a standard JUnit-style test is way easier to write, but I guess that depends on what you’re used to.

Having read about Mozilla Rhino several times, a JavaScript interpreter written in Java with good integration between the two languages, I set out to see how much work it could be to write a simple test framework. As it turned out, it wasn’t much work at all, and there’s so little code that I can show all of it in this post.

Not only the helper classes, but also the tests themselves are written in Java, not JavaScript. The JS code to execute is passed around as Strings. That may not seem very nice, but it was very easy to implement and I don’t find it poses a problem. It’s limited in size and you don’t need to worry about syntax errors, as that code is what you’re testing in the first place.

Let’s look at a test first to see how it’s used, that’s the important part after all. We augmented the JS String class with a trim() method that’s supposed to behave like the one in Java. (I know that augmenting base JS classes is discouraged by Douglas Crockford, so we do it very rarely.)

We test our String.trim() with a JUnit test extending BaseJSTestCase. In the test, we give our trim() method a couple of strings to trim, and simply assert that the result is the same as the one java.lang.String.trim() gives. That allows for a concise test by looping over the input strings, although you could of course just as well assertEquals() with a string literal.

package core;

import java.util.Arrays;
import test.BaseJSTestCase;

public class StringTest
    extends BaseJSTestCase
{

    public StringTest()
        throws Exception
    {
        super("expasy/core/String.js");
    }

    public void testTrim()
        throws Exception
    {
        for (String str : Arrays.asList(new String[] {
                "foo",
                " foo ",
                "foo bar",
                " foo bar ",
                "!foo_23",
                " !foo_ 23 "}))
        {
            assertEquals(str.trim(),
                    eval.eval("\"" + str + "\".trim();", String.class));
        }
    }
}

As you can see, we specify the JS file containing the code to test in the constructor. This encourages a policy of having one test file per JS file, which makes navigating your tests straightforward.

The interaction with the JavaScript world happens via the eval.eval() call. Apparently there’s a JS evaluator somewhere. Unsurprisingly, it’s set in the parent:

package test;

import java.io.IOException;
import junit.framework.TestCase;

public abstract class BaseJSTestCase
    extends TestCase
{
    // TODO ugly
    protected static final String SCRIPTS_PREFIX = "web/scripts/";

    private String fileUnderTest;
    protected JSEvaluator eval;

    public BaseJSTestCase(String fileUnderTest)
    {
        this.fileUnderTest = SCRIPTS_PREFIX + fileUnderTest;
    }

    protected void setUp()
        throws IOException
    {
        eval = new JSEvaluator(fileUnderTest, this.getName());
    }

    protected void tearDown()
    {
        eval.close();
        eval = null;
    }
}

Finally we’re getting to the evaluator. It’s just as simple. All the magic happens in Rhino, we only provide an interface to its Context class. We’ve seen how we pass the name of the JS file to test in our test’s constructor. Here we see how, after some plumbing, it’s read into our context via evaluateReader(). The context is stateful, so it requires re-initializing in setUp() to get a fresh one for every test. The main interface for executing tests is the public T eval(String js, Class returnType) method. It evaluates the given code and casts the result to the given type T. The conversion to the correct Java type conveniently happens inside Rhino. There’s also the convenience method public void eval(String js) for executing test setup code whose return value is not interesting, for instance initializing some variables.

package test;

import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import org.mozilla.javascript.Context;
import org.mozilla.javascript.EvaluatorException;
import org.mozilla.javascript.Scriptable;

public class JSEvaluator
{
    private Context context = Context.enter();
    private Scriptable scope;
    private String forTest;

    JSEvaluator(String filename, String forTest)
        throws IOException
    {
        this.forTest = forTest;
        context = Context.enter();

        scope = context.initStandardObjects();
        context.evaluateReader(scope,
                new FileReader(new File(filename)), filename, 1, null);
    }

    private Object evaluate(String js)
    {
        return context.evaluateString(scope, js, forTest, 1, null);
    }

    public void eval(String js)
    {
        evaluate(js);
    }

    @SuppressWarnings("unchecked")
    public  T eval(String js, Class returnType)
        throws JSEvaluatorException
    {
        try
        {
            return (T) Context.jsToJava(evaluate(js), returnType);
        }
        catch (EvaluatorException e)
        {
            throw new JSEvaluatorException(e.getScriptStackTrace());
        }
    }

    void close()
    {
        Context.exit();
    }

    @SuppressWarnings("serial")
    public class JSEvaluatorException
        extends RuntimeException
    {
        JSEvaluatorException(String info)
        {
            super(info);
        }
    }
}

That’s it, I hope it’s helpful and shows how easy it is to use Rhino in Java code.

About these ads

Tags: , , , , , , , ,

3 Responses to “Java-based JavaScript unit testing with Rhino”

  1. Janaki Ram Says:

    Hi,

    Thanks a lot for this post!

    I have a basic question regarding the usage of Context.jsToJava. Does this API only work for basic data types such as String, Integer, Boolean and it does not work for user defined types? I mean, can I convert user defined JS types to user defined Java types?

    Janaki

  2. thomas11 Says:

    Hi Janaki, thanks for the feedback.

    My memory is a bit vague on this because I didn’t work with this code for months. However, the Rhino API doc seems to indicate that it’s possible: http://www.mozilla.org/rhino/apidocs/org/mozilla/javascript/Context.html#jsToJava%28java.lang.Object,%20java.lang.Class%29.

    Good luck finding the LiveConnect documentation, my brief attempt wasn’t successful.

  3. Edy Hinzen Says:

    Hi Thomas!

    Thanks a lot for this post. I didn’t work with Rhino before and had (nearly) immediate succes realizing your code.
    In your sample, there are some declarations missing; I think it’s due to the BLOG-Format.
    Let me try to say it in pseudo-code.
    It reads
    public T eval(String js, Class returnType)
    but should be
    public [less-than]T[greater-than] T eval(String js, Class[less-than]T[greater-than] returnType)

    Kind regards
    Edy Hinzen

Comments are closed.


Follow

Get every new post delivered to your Inbox.

%d bloggers like this: