Unit-testing AJAX code with Maven

01 March 2010

Stéphane Épardaud

by Stéphane Épardaud

This article presents a way to use Maven to write fully-automated JavaScript integration tests that invoke RESTful services on the server using AX-RS/http://jboss.org/resteasy[RESTEasy]." ---

While adding a new feature to RESTEasy — the generation of JavaScript skeleton code to invoke RESTful web services implemented in JAX-RS — I candidly proceeded to write unit tests for that part. Unit testing this new feature meant making sure that not only was the JavaScript code produced by the web services (in our case, produced by a Servlet, and thus running in a Servlet container), I also wanted to make sure that the JavaScript code was valid, contained all the functions required and that the functions worked. Since the generated functions use AJAX to call the web services, that leaves us with the following constraints on what we need in order to test this:

  • A test framework integrated into the project’s build tool, Maven

  • JAX-RS web services whose JavaScript skeleton code to produce, and to invoke

  • A Servlet container to run those JAX-RS web services, as well as my JavaScript generator Servlet

  • A JavaScript interpreter that I can use in Java (since this is the RESTEasy implementation language)

  • A JavaScript interpreter that also can do AJAX

  • A JavaScript interpreter that supports JavaScript 1.8 (since I use a feature introduced in that version: native JSON)

##Choosing the proper tools

There are many diverse and good JavaScript interpreters, but only one of them is both done in Java and allows us to interact with it in Java: Rhino.

There seems to be little evidence on Rhino’s documentation that it supports AJAX, even though Canoo (although in fact its underlying HTML engine HtmlUnit), which uses Rhino as JavaScript engine, claims to support AJAX (which could be an HtmlUnit addition). HtmlUnit (through Canoo) is available as a Maven plugin for unit tests, but it is really aimed at HTML rendering and testing, and requires the tests to be written in a procedural form of XML which we do not particularly like.

There is another project mentioning the addition of AJAX to Rhino, which is Env.js: a JavaScript implementation of AJAX and other browser-related additions to Rhino (Rhino implements JavaScript the language, and not the browser APIs and objects that are usually browser additions). A quick search for a Maven plugin comes up with testlol which claims to be able to run JavaScript unit tests (with AJAX) from Maven. This seems to be the best bet so far, and it even comes with a JUnit-like test framework in JavaScript for my tests.

Now about the server part, I am testing RESTEasy, and it already has several tests that deploy RESTEasy in Jetty for integration tests in Maven using the Maven Jetty plugin, so I will be using this setup as well.

Our winners are thus:

  • Rhino

  • Env.js

  • Testlol

  • Jetty

Setting up the tools

Rhino

The latest Rhino version released is 1.7R2 which has been out since March 2009, and is available on Maven’s central repository but the trick is that we are using the JSON native parser that has been specified in JavaScript 1.8.1 and ECMAScript 5.0 (the underlying specification of JavaScript).

Alas, Rhino 1.7R2 only implements JavaScript 1.7, while the summer of 2009 saw a Google Summer Of Code for Rhino that aimed at providing Rhino with ECMAScript 5.0 (including the JSON native object). This work has now been completed and is included in the current Rhino trunk for 1.7R3, which means we have to build our own Rhino jar in order to use those goodies:

$ cvs -d ':pserver:anonymous@cvs-mirror.mozilla.org:/cvsroot' co mozilla/js/rhino rhino
$ cd rhino
$ ant jar
$ mvn install:install-file -Dfile=build/rhino1_7R3pre/js.jar -DgroupId=rhino \
    -DartifactId=js -Dversion=1.7R3 -Dpackaging=jar

We now have a JSON-enabled Rhino version in our local Maven repository.

Testlol

Testlol is the Maven plugin that lets us run AJAX-enabled JavaScript unit tests in Maven. When I started using it, it did not support loading JavaScript files from HTTP URLs, only FILE URLs (from the local file system), which in my case was useless since a Servlet generates the JavaScript API. There also was a bug in Env.js which made it impossible to instantiate the XmlHttpRequest object, which I had to fix.

In order to fit our needs, I created a fork of testlol (with support for HTTP script URLs and a fixed XmlHttpRequest object) in github which you can use to build testlol:

$ git clone git://github.com/FroMage/testlol.git
$ cd testlol
$ mvn install

We now have all the tools ready in our local Maven repository to write the first AJAX-enabled JavaScript unit tests.

Writing our unit tests

The setup

In order to test our AJAX code, we need to:

  • Write a JAX-RS resource which will be invoked by the tests (MyResource.java)

  • Run RESTEasy within Jetty, to serve the JAX-RS resource (configured in web.xml)

  • Run testlol to test AJAX methods (test.js and test.html)

The following directory structure is proposed:

/
+- src
+  +-- main
+  +   +--- java
+  +   +    +----- MyResource.java
+  +   +--- webapp
+  +   +    +----- WEB-INF
+  +   +    +      +------ web.xml
+  +   +--- test.html
+  +-- test
+  +   +--- js
+  +   +    +--- test.js
+- pom.xml

The Maven POM

This Maven POM will cause all your JS unit tests run in the integration-test phase:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
                             http://maven.apache.org/maven-v4_0_0.xsd">
 <modelVersion>4.0.0</modelVersion>
 <groupId>org.jboss.resteasy.test</groupId>
 <artifactId>jsapi-servlet-test</artifactId>
 <packaging>war</packaging>
 <version>1.3.RC1-SNAPSHOT</version>
 <name>jsapi-servlet-test</name>

 <dependencies>
  <!-- ... -->
 </dependencies>
 <build>
  <finalName>jsapi-servlet-test</finalName>
  <plugins>
   <!-- The Jetty plugin which will run our JAX-RS resources -->
   <plugin>
    <groupId>org.mortbay.jetty</groupId>
    <artifactId>maven-jetty-plugin</artifactId>
    <version>6.1.15</version>
    <configuration>
     <!-- By default the artifactId is used,
          override it with something simple -->
     <contextPath>/</contextPath>
     <scanIntervalSeconds>2</scanIntervalSeconds>
     <stopKey>foo</stopKey>
     <stopPort>9999</stopPort>
     <connectors>
      <connector
        implementation="org.mortbay.jetty.nio.SelectChannelConnector">
       <port>9095</port>
       <maxIdleTime>60000</maxIdleTime>
      </connector>
     </connectors>
    </configuration>
    <executions>
     <execution>
      <id>start-jetty</id>
      <phase>pre-integration-test</phase>
      <goals>
       <goal>run</goal>
      </goals>
      <configuration>
       <scanIntervalSeconds>0</scanIntervalSeconds>
       <daemon>true</daemon>
      </configuration>
     </execution>
     <execution>
      <id>stop-jetty</id>
      <phase>post-integration-test</phase>
      <goals>
       <goal>stop</goal>
      </goals>
     </execution>
    </executions>
   </plugin>
   <!-- We need annotations -->
   <plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <configuration>
     <source>1.5</source>
     <target>1.5</target>
    </configuration>
   </plugin>
   <!-- The testlol plugin which runs our JS unit tests -->
   <plugin>
    <groupId>tv.bodil</groupId>
    <artifactId>maven-testlol-plugin</artifactId>
    <version>1.1</version>
    <dependencies>
     <!-- Force rhino version 1.7RC3 -->
     <dependency>
      <groupId>rhino</groupId>
      <artifactId>js</artifactId>
      <version>1.7R3</version>
     </dependency>
    </dependencies>
    <executions>
     <execution>
      <phase>integration-test</phase>
      <goals>
       <goal>test</goal>
      </goals>
      <configuration>
       <basePath>src/test/html</basePath>
       <testSuite>src/test/js</testSuite>
      </configuration>
     </execution>
    </executions>
   </plugin>
  </plugins>
 </build>
</project>

The JAX-RS resource

Let’s make a very simple JAX-RS resource which responds to the GET method with "ok":

import javax.ws.rs.GET;
import javax.ws.rs.Path;

@Path("/")
public class MyResource{

 @GET
 public String get(){
  return "ok";
 }
}

While we wait for a Servlet 3.0 implementation of RESTEasy, we still have to write the following web.xml file to enable RESTEasy and the JavaScript API I am writing:

<!DOCTYPE web-app PUBLIC
  "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
  "http://java.sun.com/dtd/web-app_2_3.dtd" >

<web-app>
 <display-name>JS API test Web Application</display-name>

 <!-- The resource to test -->
 <context-param>
  <param-name>resteasy.resources</param-name>
  <param-value>MyResource</param-value>
 </context-param>

 <!-- The prefix for all REST resources -->
 <context-param>
  <param-name>resteasy.servlet.mapping.prefix</param-name>
  <param-value>/rest</param-value>
 </context-param>

 <!-- The RESTEasy listener -->
 <listener>
  <listener-class>org.jboss.resteasy.plugins.server.servlet.ResteasyBootstrap</listener-class>
 </listener>

 <!-- The RESTEasy JS API servlet -->
 <servlet>
  <servlet-name>Resteasy JSAPI</servlet-name>
  <servlet-class>org.jboss.resteasy.jsapi.JSAPIServlet</servlet-class>
 </servlet>

 <servlet-mapping>
  <servlet-name>Resteasy JSAPI</servlet-name>
  <url-pattern>/rest-js</url-pattern>
 </servlet-mapping>

 <!-- The RESTEasy servlet -->
 <servlet>
  <servlet-name>Resteasy</servlet-name>
  <servlet-class>org.jboss.resteasy.plugins.server.servlet.HttpServletDispatcher</servlet-class>
 </servlet>

 <servlet-mapping>
  <servlet-name>Resteasy</servlet-name>
  <url-pattern>/rest/*</url-pattern>
 </servlet-mapping>

</web-app>

The JavaScript test file

Since testlol requires that I load an HTML file (in my case I’m only testing JavaScript, so loading a JavaScript file would have been sufficient), I need an HTML file which loads the relevant JavaScript API I am testing:

<html>
 <head>
  <script src="/rest-js" type="text/javascript"></script>
 </head>
 <body>
 </body>
</html>

Then I can write my tests using testlol:

/* Load the test.html file */
Envjs('http://localhost:9095/test.html', {
 /* Tell testlol to load external scripts */
 scriptTypes : {
  "text/javascript"   : true
 }
});

/* Check that our API functions are defined */
function testFunctions() {
 assertNotNull("get function", MyResource.get);
}

/* Check that we can call our AJAX method */
function testGet() {
 var data = MyResource.get();
 assertEquals("ok", data);
}

And voilà, that is all there is to it, just type mvn integration-test and in the middle of tons of useless verbosity you will see if your tests pass or not (in our case they do).

Conclusion

It took me quite a bit of time to find the appropriate tools and setup, but now I am very happy to have this working, and I hope to be able to reuse this setup in other projects. The only problem right now is that I cannot commit this to RESTEasy just yet since neither testlol nor Rhino 1.7R3 are in any Maven repository, but I am working on this and hope to resolve the issue ASAP.