Kevin's Worklog

Documenting the Daily Stream

Configuring WebDriver PhantomJS Testing in Maven

I’ve used JUnit for testing for awhile now, but recently I’ve been wanting to learn a little more about other testing tools and, in particular, tools that support the testing of JavaScript applications. I’ve dabbled with Selenium in the past, but the learning curve (including setup time) has been daunting.

I’m a big fan of things that run as a part of a Maven build with very little effort on my part. To this end, I started experimenting with Selenium’s WebDriver implementation. After trying out the HTMLUnitDriver, I decided that I wanted to use PhantomJS instead (since its code is based on an actual browser’s JavaScript implementation).

What follows is what I’ve come up with as a workable Maven configuration.

For the purposes of this post, I’m going to ignore the unit testing configuration and just focus on the integration testing. As a part of the integration testing, I’ll run code that tests content that’s being served from a Jetty server within the processes of the Maven build.

Usually PhantomJS requires that you download its binary and point your integration test runner to it, but luckily I found a Maven plugin that will do that work for me. The first thing I want to do, though, is to configure the WebDriver that I plan to use, GhostDriver.

I’ll also configure JUnit because I’m going to use that to write my integration tests. Here are the relevant parts of my pom.xml file:

<properties>
    <junit.version>4.12-beta-1</junit.version>
    <ghostdriver.version>1.1.0</ghostdriver.version>
</properties>

<dependencies>
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>${junit.version}</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>com.github.detro.ghostdriver</groupId>
        <artifactId>phantomjsdriver</artifactId>
        <version>${ghostdriver.version}</version>
        <scope>test</scope>
    </dependency>
</dependencies>

Next, I’ll want to configure the phantomjs-maven-plugin (that will download PhantomJS for me). What the below does, in addition to downloading PhantomJS, is put the path to its executable in a phantomjs.binary property. This can then be passed via configuration as a system property for the integration test framework (an example of this can be seen in the code block after the one below).

<properties>
    <junit.version>4.12-beta-1</junit.version>
    <ghostdriver.version>1.1.0</ghostdriver.version>
    <phantomjs.plugin.version>0.4</phantomjs.plugin.version>
</properties>

<build>
    <plugins>
        <!-- Installs PhantomJS so it doesn't have to be pre-installed -->
        <plugin>
            <groupId>com.github.klieber</groupId>
            <artifactId>phantomjs-maven-plugin</artifactId>
            <version>${phantomjs.plugin.version}</version>
            <executions>
                <execution>
                    <goals>
                        <goal>install</goal>
                    </goals>
                </execution>
            </executions>
            <configuration>
                <version>${phantomjs.version}</version>
            </configuration>
        </plugin>
    </plugins>
</build>

In order to run the tests, we’ll need an integration test framework (in our case, Failsafe) and Jetty to serve our test files. We can also use the build-helper-maven-plugin to find ports on the local system that are available for Jetty to use.

The following plugins should be added to the pom.xml file’s build/plugins element. The configuration of the phantomjs.binary property can also be seen below.

<!-- Get two free ports for our test server to use -->
<plugin>
    <groupId>org.codehaus.mojo</groupId>
    <artifactId>build-helper-maven-plugin</artifactId>
    <version>1.6</version>
    <configuration>
        <portNames>
            <portName>jetty.port</portName>
            <portName>jetty.port.stop</portName>
        </portNames>
    </configuration>
    <executions>
        <execution>
            <id>reserve-port</id>
            <phase>initialize</phase>
            <goals>
                <goal>reserve-network-port</goal>
            </goals>
        </execution>
    </executions>
</plugin>
<!-- Use failsafe to run our integration tests -->
<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-failsafe-plugin</artifactId>
    <version>${failsafe.version}</version>
    <configuration>
        <systemPropertyVariables>
            <phantomjs.binary>${phantomjs.binary}</phantomjs.binary>
        </systemPropertyVariables>
    </configuration>
    <executions>
        <execution>
            <id>integration-test</id>
            <goals>
                <goal>integration-test</goal>
            </goals>
        </execution>
        <execution>
            <id>verify</id>
            <goals>
                <goal>verify</goal>
            </goals>
        </execution>
    </executions>
</plugin>
<!-- Use a Jetty test server to serve our test content -->
<plugin>
    <groupId>org.eclipse.jetty</groupId>
    <artifactId>jetty-maven-plugin</artifactId>
    <version>${jetty.version}</version>
    <dependencies>
        <dependency>
            <groupId>org.eclipse.jetty</groupId>
            <artifactId>jetty-servlet</artifactId>
            <version>${jetty.version}</version>
        </dependency>
    </dependencies>
    <configuration>
        <jvmArgs>-Djetty.port=${jetty.port}</jvmArgs>
        <stopKey>ANY_KEY_HERE_IS_FINE</stopKey>
        <stopPort>${jetty.port.stop}</stopPort>
    </configuration>
    <executions>
        <execution>
            <id>start-jetty</id>
            <phase>pre-integration-test</phase>
            <goals>
                <goal>run</goal>
            </goals>
            <configuration>
                <daemon>true</daemon>
            </configuration>
        </execution>
        <execution>
            <id>stop-jetty</id>
            <phase>post-integration-test</phase>
            <goals>
                <goal>stop</goal>
            </goals>
        </execution>
    </executions>
</plugin>

We’ll also need to add some more properties to the pom.xml file to handle the variables in the above plugin configurations.

<properties>
    <jetty.version>9.2.2.v20140723</jetty.version>
    <failsafe.version>2.17</failsafe.version>
    <junit.version>4.12-beta-1</junit.version>
    <ghostdriver.version>1.1.0</ghostdriver.version>
    <phantomjs.version>1.9.7</phantomjs.version>
    <phantomjs.plugin.version>0.4</phantomjs.plugin.version>
</properties>

Once this is all configured, the PhantomJS driver can be accessed via your JUnit-based Java code.

private static String PHANTOMJS_BINARY;

/**
 * Check that the PhantomJS binary was installed successfully.
 */
@BeforeClass
public static void beforeTest() {
    PHANTOMJS_BINARY = System.getProperty("phantomjs.binary");

    assertNotNull(PHANTOMJS_BINARY);
    assertTrue(new File(PHANTOMJS_BINARY).exists());
}

/**
 * Test something.
 */
@Test
public void testSomething() {
    final DesiredCapabilities capabilities = new DesiredCapabilities();
    final String port = System.getProperty("jetty.port");

    // Configure our WebDriver to support JavaScript and be able to find the PhantomJS binary
    capabilities.setJavascriptEnabled(true);
    capabilities.setCapability("takesScreenshot", false);
    capabilities.setCapability(
        PhantomJSDriverService.PHANTOMJS_EXECUTABLE_PATH_PROPERTY,
        PHANTOMJS_BINARY
    );

    final WebDriver driver = new PhantomJSDriver(capabilities);
    final String baseURL = "http://localhost:" + port;

    if (port == null) {
        fail("System property 'jetty.port' is not set");
    }

    // If the referenced JavaScript files fail to load, the test fails at this point
    driver.navigate().to(baseURL + "/index.html");

    // Then do some more tests using WebDriver methods...
}

That’s about it… a relatively simple way to run integration tests against JavaScript sources using Maven and JUnit.