May 31, 2011

Maven + JavaScript Unit Test using JsTestDriver

What are we doing?
You wake up to the annoying Justin Timberlake ringtone you set for the office only to discover it's the site is on fire" call. A quick glance at the alarm clock and you realize it's 3am. It's this time when you decide that you need to get some test coverage on your Java Script.

What is Unit Testing?
Unit Testing is a method by which individual units of source code are tested to determine if they are fit for use. A unit is the smallest testable part of an application and in JavaScript, typically it is a function.

Why would you want to unit test JavaScript?
JavaScript has grown from the little scripts that would change button images on rollover to a fully functional, and often application critical, complex applications. There are many interactions with DOM objects, AJAX request/response and user events that make testing your code a must-have. Unit testing will give you the the confidence to add new features and change existing code. In addition, unit test self-document the code so you can be sure that the rad new function you wrote does exactly what you want it to.

What is covered
What the rest of this blog will describe is one approach to integrate maven and JsTestDriver for local development. In a near future blog, this approach will be extended to use on a Linux/Unix based system for a continuous integration system.

Approach
There are many approaches to unit testing JavaScript (QUnit being one of the many), however, most of the unit test frameworks do not easily allow you to integrate your unit test as a part of a maven testing cycle. The one problem with all JavaScript unit testing frameworks is that you need to have a place to run your test. At the time of this blog, there is not a "headless" browser, thus you (or your framework) must open a browser, accept JavaScript loading and running the test cases while providing repeatable testing output. This blog series will discuss a few different methods to run unit test, including a method to run in a virtual framebuffer for continuous integration systems that do not allow you to open a browser in the default display.

What is JsTestDriver?
JsTestDriver is an small application that controls your test cases, your source files and the interaction between the browsers you choose to test against. JsTestDriver allows you to only load the source files you want, along with only the DOM elements you need to unit test your source code, in other words, the browser ONLY loads your JavaScript files and optionally HTML and/or DOM elements needed by your test.

How to integrate with Maven
The approach here is to add JavaScript testing via a profile, so you can create a CI build specifically for your JavaScript unit test.

  1. JsTestDriver Setup

    This blog will not discuss in depth how to setup and run JsTestDriver. For more in-depth information, please visit http://code.google.com/p/js-test-driver/ website. This blog is using JsTestDriver 1.3.2.

    In order to load your test scripts, you must create a configuration file. For a maven application, place your configuration file in your "/resources" folder.

    Here is an example properties file (jsTestDriver.conf)

    server: http://localhost:4220
    
    load:
     # Header JS Files, all files are loaded top-down
     - main-js/lib/jquery.min.js
     # Body JS files
     - main-js/Person.js
     # Test files
     - test-js/*.js
    
    exclude:
     - main-js/exclude.js
    
    

    The above configuration file assumes your file structure is similar to:
    /src/main/webapp/scripts/*.js
    /src/test/webapp/resources/jsTestDriver.conf
    /src/test/webapp/scripts/*.js

    (NOTE: You might notice that the file path is different than the "main-js". Keep reading!)

  2. Maven Dependencies and Profile

    Next, let's add the dependencies to the POM file in your project.

    Add the following to your "dependencies" section in your pom.xml
    <dependency>
     <groupid>com.google.jstestdriver</groupId>
     <artifactid>jstestdriver</artifactId>
     <version>1.2.2</version>
     <scope>test</scope>
     <type>jar</type>
    </dependency>
    
    

    Next, we need to add a profile, which will do quite a few things. First, the JsTestDriver jar file will be copied from the repository to a known folder in the /target folder for use when running the JsTestDriver server. Next, we need to copy the JavaScript files from your project's location to a known folder in the "/test" folder, and also to match the "main-js/*" and "test-js/*" folder structures that we setup in the jsTestDriver.conf file. Lastly, we need to startup the JsTestDriver server so we can run our test.

    Here is a profile to start with:

    <profile>
        <id>local-jstests</id>
        <build>
            <plugins>
    
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-dependency-plugin</artifactId>
                    <version>2.1</version>
                    <executions>
                        <execution>
                            <id>copy</id>
                            <phase>generate-resources</phase>
                            <goals>
                                <goal>copy</goal>
                            </goals>
                            <configuration>
                                <artifactItems>
                                    <artifactItem>
                                        <groupId>com.google.jstestdriver</groupId>
                                        <artifactId>jstestdriver</artifactId>
                                        <version>${js-test-driver.version}</version>
                                        <type>jar</type>
                                        <overWrite>true</overWrite>
                                        <destFileName>jsTestDriver.jar</destFileName>
                                    </artifactItem>
                                </artifactItems>
                                <outputDirectory>${project.build.directory}/jstestdriver</outputDirectory>
                                <overWriteReleases>false</overWriteReleases>
                                <overWriteSnapshots>true</overWriteSnapshots>
                            </configuration>
                        </execution>
                    </executions>
                </plugin>
    
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-surefire-plugin</artifactId>
                    <configuration>
                        <skipTests>true</skipTests>
                    </configuration>
                </plugin>
    
                <plugin>
                    <artifactId>maven-resources-plugin</artifactId>
                    <version>2.4.3</version>
                    <executions>
                        <execution>
                            <id>copy-main-files</id>
                            <phase>generate-test-resources</phase>
                            <goals>
                                <goal>copy-resources</goal>
                            </goals>
                            <configuration>
                                <outputDirectory>${basedir}/target/test-classes/main-js</outputDirectory>
                                <resources>
                                    <resource>
                                        <directory>src/main/webapp/scripts</directory>
                                        <filtering>false</filtering>
                                    </resource>
                                </resources>
                            </configuration>
                        </execution>
                        <execution>
                            <id>copy-test-files</id>
                            <phase>generate-test-resources</phase>
                            <goals>
                                <goal>copy-resources</goal>
                            </goals>
                            <configuration>
                                <outputDirectory>${basedir}/target/test-classes/test-js</outputDirectory>
                                <resources>
                                    <resource>
                                        <directory>src/test/webapp/scripts</directory>
                                        <filtering>false</filtering>
                                    </resource>
                                </resources>
                            </configuration>
                        </execution>
                    </executions>
                </plugin>
    
                <plugin>
                    <artifactId>maven-antrun-plugin</artifactId>
                    <version>1.6</version>
                    <executions>
                        <execution>
                            <phase>test</phase>
                            <configuration>
    
                                <target>
                                    <!-- make output folder -->
                                    <mkdir dir="${basedir}/target/js-test-results"/>
                                    <!-- start jsTestDriver server -->
                                    <exec executable="java">
                                        <arg line="-jar ${basedir}/target/jstestdriver/jsTestDriver.jar"/>
                                        <arg line="--port 4220"/>
                                        <!-- Possibly set this up to point to the same location of your surefire output -->
                                        <arg line="--testOutput ${basedir}/target/js-test-results"/>
                                        <arg line="--config ${basedir}/target/test-classes/jsTestDriver.conf"/>
                                        <arg line="--browser /opt/google/chrome/google-chrome"/>
                                        <arg line="--tests all"/>
                                        <arg line="--verbose"/>
                                        <arg line="--captureConsole"/>
                                        <arg line="--preloadFiles"/>
                                    </exec>
                                </target>
                            </configuration>
                            <goals>
                                <goal>run</goal>
                            </goals>
                        </execution>
                    </executions>
    
                </plugin>
            </plugins>
        </build>
    </profile>
    
    

  3. Sample Unit Test

    Now you need to have a sample JavaScript file and a corresponding unit test.

    Here is a simple JavaScript file (/src/main/webapp/scripts/Person.js)

    NAMESPACE = {};
     
    NAMESPACE.Person = function(initObj) {
     this.name = initObj.name;
     this.age = initObj.age;
     this.address = initObj.address;
     this.sex = initObj.sex;
     this.significantOther = initObj.significantOther;
     
     function setSignificantOther(person) {
      this.significantOther = person;
     }
     
     this.setSignificantOther = setSignificantOther;
    }
    

    And a sample test case might be: (/src/test/webapp/scripts/PersonTest.js

    TestCase("PersonTest", {
     testA:function(){
      expectAsserts(1);
    
       var peterGriffin = new NAMESPACE.Person({
        name: "Peter Griffin",
        age: 43,
        address: {
         address1: "31 Spooner Street",
         address2: "",
         city: "Quhog",
         state:"RI"
        },
        sex: "Male",
        significantOther: null
       });
       
       assertNotNull("The variable should not be null", peterGriffin);
      };
     }
    });
    
    
  4. Running your test

    In order to run your test, you can now type:

    mvn clean test -Plocal-js


Please stay tuned for the expansion to this on how to run in a virtual framebuffer for a CI environment (and of course, updates for found errors)