Custom Development

Automating Javascript Unit Tests in Maven Project

Jeff Zapotoczny

In the not-too-distant past, almost nobody unit tested javascript code. Whether this was because the js code seen in most apps consisted of thin little scripts that didn't do much of anything important or because developers simply didn't know how to write js unit tests no longer matters. The fact is that an increasing amount of logic and complexity in modern applications lives in javascript code, and failing to unit test it borders on neglect.

My purpose here isn't to sermonize on unit testing or even to discuss how to write such tests -- there are numerous libraries available and many of the popular ones have extensive documentation. I happened to utilize QUnit on a recent project and it met my needs (testing some modularized js code utilized across a number of pages) quite well. In the case of QUnit, tests consist of a js test file coupled with an HTML file that loads the script under test as well as the test script. Running the tests manually is a matter of simply viewing the HTML file in a browser.

That's great for one-off test runs, but one of the real advantages of having a battery of unit tests is being able to automate the running of those tests so they can catch failures even when you don't realize you may have altered system behavior. If you're working on maintaining or modernizing an enterprise application, chances are you've got an automated build. At least I hope you do. If you're in a Java ecosystem, likely your automation consists of maven as the build agent with the surefire plugin running JUnit tests, combined with a continuous integration system like Jenkins that knows about your maven build and invokes it periodically or on version control commits. This combination is so popular that most continuous integration products have affordances that parse the surefire-generated test reports and give you nice visualizations of your test results.

So how can you approach getting your new javascript unit tests integrated into such a build process with the same level of support that JUnit has? Initial searches (at least as of mid-2014) will reveal that there are dozens of shallow solutions but no clear winner. There are maven plugins that will run your tests with a simple pass/fail and a few that will even generate output compatible with the aforementioned surefire reports. I spent a lot of time on investigation and the solution that seemed to fit best involved js-testrunner. The authors of js-testrunner solved the problem cleverly by writing a JUnit test runner that itself can serve up the test files using jetty and evaluate the tests by loading them in a headless web browser (phantomjs). Because the test itself is a JUnit test, existing reporting and support for JUnit just works. The only problem is that they went a little light on documenting all the moving parts. Hence this blog post.

Setting it Up

js-testrunner itself can be added to your project as a dependency like so:

<code>&lt;dependency&gt;
  &lt;groupId&gt;org.codehaus.jstestrunner&lt;/groupId&gt;
  &lt;artifactId&gt;jstestrunner-junit&lt;/artifactId&gt;
  &lt;version&gt;1.0.2&lt;/version&gt;
  &lt;scope&gt;test&lt;/scope&gt;
&lt;/dependency&gt;</code>

But you'll also need to get jetty into the picture. This is how you get your regular resources and test resources unified together into a browse-able set of unified content. That's done as a build plugin like so:

<code>&lt;plugin&gt;
  &lt;groupId&gt;org.mortbay.jetty&lt;/groupId&gt;
  &lt;artifactId&gt;jetty-maven-plugin&lt;/artifactId&gt;
  &lt;version&gt;8.1.14.v20131031&lt;/version&gt;
  &lt;configuration&gt;
    &lt;webAppConfig&gt;
      &lt;contextPath&gt;/&lt;/contextPath&gt;
      &lt;resourceBases&gt;
        &lt;resourceBase&gt;${project.build.outputDirectory}&lt;/resourceBase&gt;
        &lt;resourceBase&gt;${project.build.testOutputDirectory}&lt;/resourceBase&gt;
      &lt;/resourceBases&gt;
    &lt;/webAppConfig&gt;
  &lt;/configuration&gt;
&lt;/plugin&gt;</code>

Now you need some way to browse it headlessly: enter phantomjs. Unlike jetty, phantomjs is a platform-dependent executable with a separate install. So rather than including it as a maven dependency, we need to satisfy that dependency externally and then tell our build about it. js-testrunner's documentation covers a strategy for externalizing the command pattern used to invoke phantomjs as part of the surefire configuration:

<code> &lt;plugin&gt;
   &lt;groupId&gt;org.apache.maven.plugins&lt;/groupId&gt;
   &lt;artifactId&gt;maven-surefire-plugin&lt;/artifactId&gt;
   &lt;version&gt;2.16&lt;/version&gt;
   &lt;configuration&gt;
     &lt;systemPropertyVariables&gt;
       &lt;org.codehaus.jstestrunner.commandPattern&gt;
         ${org.codehaus.jstestrunner.commandPattern}
       &lt;/org.codehaus.jstestrunner.commandPattern&gt;
     &lt;/systemPropertyVariables&gt;
   &lt;/configuration&gt;
 &lt;/plugin&gt;
			</code>

This is fine, but then they only show how to set that command pattern property from outside maven. I didn't like the idea of other developers needing to know about this requirement, nor did I want to have to alter our Jenkins configuration just to set this. Additionally, I had a need to run these tests on two possible platforms (windows and linux) which would mean two sets of command pattern syntax. And to really make things complicated, I had the extra constraint of not having privileges to add new software to the linux host running Jenkins, so it was time to get clever. I decided, since they're small, to actually add the phantomjs executables for each platform to the project as test resources under source control, and use maven's profiles capability to detect which OS is running maven and set itself up to run phantomjs successfully.

<code>
&lt;profiles&gt;
  &lt;profile&gt;
    &lt;id&gt;windows&lt;/id&gt;
    &lt;activation&gt;
      &lt;os&gt;
        &lt;family&gt;Windows&lt;/family&gt;
      &lt;/os&gt;
    &lt;/activation&gt;
    &lt;properties&gt;
      &lt;org.codehaus.jstestrunner.commandPattern&gt;target\test-classes\phantomjs\phantomjs.exe '%1$s' %2$s&lt;/org.codehaus.jstestrunner.commandPattern&gt;
    &lt;/properties&gt;
  &lt;/profile&gt;
</code>

That's all that's needed on Windows. For linux, you have to consider the additional complexity of setting the checked-out phantomjs file executable before js-testrunner attempts to execute it. I accomplished this within the linux profile by using maven's exec plugin to run the chmod command on the file to alter the privileges.

<code>
  &lt;profile&gt;
    &lt;id&gt;linux&lt;/id&gt;
    &lt;activation&gt;
      &lt;os&gt;
        &lt;family&gt;unix&lt;/family&gt;
      &lt;/os&gt;
    &lt;/activation&gt;
    &lt;properties&gt;
      &lt;org.codehaus.jstestrunner.commandPattern&gt;target/test-classes/phantomjs/phantomjs '%1$s' %2$s&lt;/org.codehaus.jstestrunner.commandPattern&gt;
    &lt;/properties&gt;
    &lt;build&gt;
      &lt;!-- Need to set phantomjs as executable on linux before we try to run it as part of the javascript unit tests --&gt;
      &lt;plugins&gt;
        &lt;plugin&gt;
          &lt;groupId&gt;org.codehaus.mojo&lt;/groupId&gt;
          &lt;artifactId&gt;exec-maven-plugin&lt;/artifactId&gt;
          &lt;version&gt;1.3&lt;/version&gt;
          &lt;executions&gt;
            &lt;execution&gt;
              &lt;id&gt;script-chmod&lt;/id&gt;
              &lt;!-- Needs to happen after test resource copying but prior to running --&gt;
              &lt;phase&gt;process-test-classes&lt;/phase&gt;
              &lt;goals&gt;
                &lt;goal&gt;exec&lt;/goal&gt;
              &lt;/goals&gt;
              &lt;configuration&gt;
                &lt;executable&gt;chmod&lt;/executable&gt;
                &lt;arguments&gt;
                  &lt;argument&gt;+x&lt;/argument&gt;
                  &lt;argument&gt;target/test-classes/phantomjs/phantomjs&lt;/argument&gt;
                &lt;/arguments&gt;
              &lt;/configuration&gt;
           &lt;/execution&gt;
         &lt;/executions&gt;
      &lt;/plugin&gt;
  &lt;/profile&gt;
&lt;/profiles&gt;
</code>

That's it as far as maven configuration is concerned. The last bit of customization I did that involved confusion was the JUnit test itself. js-testrunner shows a simple example of an empty test that utilizes only the @RunWith annotation. That's fine if you want to run all HTML files in test-classes as tests, but you may need to restrict that. I also needed to find a way to run Jetty on an alternate port and have js-testrunner know where to find it. Both can be accomplished within the test class itself.

<code>
package com.company;
import org.codehaus.jstestrunner.junit.JSTestSuiteRunner;
import org.junit.runner.RunWith;

@RunWith(JSTestSuiteRunner.class)
@JSTestSuiteRunner.Include(value={"Test1.html", "Test2.html"})
@JSTestSuiteRunner.Host(value="localhost:9999")
public class JavascriptTest {
}</code>

And that's it. There's nothing more to the test class, as js-testrunner takes care of everything else.

Of course, now you need to write some tests. As I stated above, I happened to utilize QUnit, but I think this approach could work for automating any js unit tests that utilize the same strategy of running tests by browsing to HTML files.

Jeff Zapotoczny
ABOUT THE AUTHOR

Distinguished Technical Consultant