Integrate HtmlUnit Based Tests with Apache Maven and Cargo

This blog post is a continuation of the one titled “Automating Assembly and Integration Tests with HtmlUnit” that can be read here.

In the previous post we learned how to make use of HtmlUnit API to write automated integration tests. In this post we are going to show how to integrate those tests in an Apache Maven project lifecycle while keeping a clear distinction between the unit test suite and the integration test suite.

Creating the Maven web project

First of all, we are going to repack the sample application from previous post into a Maven web project. I’m using Eclipse Helios SR2 Java EE bundle plus m2eclipse and m2eclipse-wtp plugins.

Using the wizard provided with m2eclipse, I created a new project with web archetype – there are various archetypes available and I recommend looking all of them and use the one that best suits your needs.

Once the project is created I copied the sources, configuration files and HSQLDB data files from previous post examples. I also added the needed dependencies to the POM. This is the final project structure:

And these are the dependencies I added to the POM (please note the missing <build> element, we will look at it later):

<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/xsd/maven-4.0.0.xsd">

    <modelVersion>4.0.0</modelVersion>

    <name>sdc.samples.htmlunit</name>
    <groupId>com.accenture.sdc</groupId>
    <artifactId>sdc.samples.htmlunit</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>war</packaging>

    <dependencies>
        <dependency>
            <groupId>javax</groupId>
            <artifactId>javaee-api</artifactId>
            <version>6.0</version>
            <scope>provided</scope>
        </dependency>
        <dependency> <!-- tomcat does not include jstl implementation -->
            <groupId>javax.servlet</groupId>
            <artifactId>jstl</artifactId>
            <version>1.2</version>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-beans</artifactId>
            <version>2.5.4</version>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>2.5.4</version>
            <type>jar</type>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context-support</artifactId>
            <version>2.5.4</version>
            <type>jar</type>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-core</artifactId>
            <version>2.5.4</version>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-orm</artifactId>
            <version>2.5.4</version>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webmvc</artifactId>
            <version>2.5.4</version>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate</artifactId>
            <version>3.2.6.ga</version>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aop</artifactId>
            <version>2.5.4</version>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
            <version>2.5.4</version>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-annotations</artifactId>
            <version>3.3.0.GA</version>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-commons-annotations</artifactId>
            <version>3.3.0.GA</version>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>commons-collections</groupId>
            <artifactId>commons-collections</artifactId>
            <version>3.2</version>
            <type>jar</type>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>commons-dbcp</groupId>
            <artifactId>commons-dbcp</artifactId>
            <version>1.1</version>
            <type>jar</type>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>hsqldb</groupId>
            <artifactId>hsqldb</artifactId>
            <version>1.8.1.2</version>
            <type>jar</type>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.8.2</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>net.sourceforge.htmlunit</groupId>
            <artifactId>htmlunit</artifactId>
            <version>2.8</version>
            <scope>test</scope>
        </dependency>
    </dependencies>
</project>

Until now, except for the test code and JUnit and HtmlUnit dependencies in POM, we’ve not done anything different from any other Spring web application. In fact the application can now be deployed and executed in any container of your choice – in my case I prefer Tomcat 7:

Running the tests from Maven

With the application deployed, we are able to run the test from IDE as shown in the previous post but, could it be done from Maven as well? How can I keep separated unit tests from integration tests?

The are different ways for doing that and a good summary can be read in the Maven documentation here. As I am ultimately looking into a way of having the tests executed on continuous integration, gather code coverage metrics from them, and keep test results and coverage metrics separated from those of unit tests, what I am going to do is to configure the Maven Failsafe plug-in.

The Failsafe plug-in is a fork to the Surefire plug-in so we can easily configure a second test suite execution. The Failsafe plug-in is usually bound to the integration-test and verify goals. Given that in Maven we only have one folder for all the tests, we will need to add patterns to filter the tests.

Thus, I added both Surefire and Failsafe plug-ins to the POM and configured the filter patterns:

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>2.3.2</version>
                <configuration>
                    <source>1.6</source>
                    <target>1.6</target>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.8.1</version>
                <configuration>
                    <includes>
                        <include>**/test/*TestCase.java</include>
                    </includes>
                    <testFailureIgnore>true</testFailureIgnore>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-failsafe-plugin</artifactId>
                <version>2.8.1</version>
                <configuration>
                    <includes>
                        <include>
                            **/integrationtest/*IntegrationTestCase.java
                        </include>
                    </includes>
                    <testFailureIgnore>true</testFailureIgnore>
                </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>
        </plugins>
    </build>

Once configured, we can run Maven’s verify goal – either in the command-line or Eclipse “Run As” menu – and get the integration test executed (given the application is still running in the web server):

And finally this is the console output from the integration test execution:

[INFO] Scanning for projects...
[INFO] ------------------------------------------------------------------------
[INFO] Building sdc.samples.htmlunit
[INFO]    task-segment: [verify]
[INFO] ------------------------------------------------------------------------
[INFO] [resources:resources {execution: default-resources}]
[WARNING] Using platform encoding (Cp1252 actually) to copy filtered resources, i.e. build is platform dependent!
[INFO] Copying 1 resource
[INFO] [compiler:compile {execution: default-compile}]
[INFO] Nothing to compile - all classes are up to date
[INFO] [resources:testResources {execution: default-testResources}]
[WARNING] Using platform encoding (Cp1252 actually) to copy filtered resources, i.e. build is platform dependent!
[INFO] skip non existing resourceDirectory c:\projects\sdc.samples\sdc.samples.htmlunit\src\test\resources
[INFO] [compiler:testCompile {execution: default-testCompile}]
[INFO] Nothing to compile - all classes are up to date
[INFO] [surefire:test {execution: default-test}]
[INFO] Surefire report directory: c:\projects\sdc.samples\sdc.samples.htmlunit\target\surefire-reports

-------------------------------------------------------
 T E S T S
-------------------------------------------------------
There are no tests to run.

Results :

Tests run: 0, Failures: 0, Errors: 0, Skipped: 0

[INFO] [war:war {execution: default-war}]
[INFO] Packaging webapp
[INFO] Assembling webapp[sdc.samples.htmlunit] in [c:\projects\sdc.samples\sdc.samples.htmlunit\target\sdc.samples.htmlunit-1.0-SNAPSHOT]
[INFO] Dependency[Dependency {groupId=javax.servlet, artifactId=jstl, version=1.2, type=jar}] has changed (was Dependency {groupId=javax.servlet, artifactId=jstl, version=1.2, type=jar}).
[INFO] Dependency[Dependency {groupId=org.springframework, artifactId=spring-beans, version=2.5.4, type=jar}] has changed (was Dependency {groupId=org.springframework, artifactId=spring-beans, version=2.5.4, type=jar}).
[INFO] Dependency[Dependency {groupId=org.springframework, artifactId=spring-context, version=2.5.4, type=jar}] has changed (was Dependency {groupId=org.springframework, artifactId=spring-context, version=2.5.4, type=jar}).
[INFO] Dependency[Dependency {groupId=org.springframework, artifactId=spring-context-support, version=2.5.4, type=jar}] has changed (was Dependency {groupId=org.springframework, artifactId=spring-context-support, version=2.5.4, type=jar}).
[INFO] Dependency[Dependency {groupId=org.springframework, artifactId=spring-core, version=2.5.4, type=jar}] has changed (was Dependency {groupId=org.springframework, artifactId=spring-core, version=2.5.4, type=jar}).
[INFO] Dependency[Dependency {groupId=org.springframework, artifactId=spring-orm, version=2.5.4, type=jar}] has changed (was Dependency {groupId=org.springframework, artifactId=spring-orm, version=2.5.4, type=jar}).
[INFO] Dependency[Dependency {groupId=org.springframework, artifactId=spring-webmvc, version=2.5.4, type=jar}] has changed (was Dependency {groupId=org.springframework, artifactId=spring-webmvc, version=2.5.4, type=jar}).
[INFO] Dependency[Dependency {groupId=org.hibernate, artifactId=hibernate, version=3.2.6.ga, type=jar}] has changed (was Dependency {groupId=org.hibernate, artifactId=hibernate, version=3.2.6.ga, type=jar}).
[INFO] Dependency[Dependency {groupId=org.springframework, artifactId=spring-aop, version=2.5.4, type=jar}] has changed (was Dependency {groupId=org.springframework, artifactId=spring-aop, version=2.5.4, type=jar}).
[INFO] Dependency[Dependency {groupId=org.springframework, artifactId=spring-jdbc, version=2.5.4, type=jar}] has changed (was Dependency {groupId=org.springframework, artifactId=spring-jdbc, version=2.5.4, type=jar}).
[INFO] Dependency[Dependency {groupId=org.hibernate, artifactId=hibernate-annotations, version=3.3.0.GA, type=jar}] has changed (was Dependency {groupId=org.hibernate, artifactId=hibernate-annotations, version=3.3.0.GA, type=jar}).
[INFO] Dependency[Dependency {groupId=org.hibernate, artifactId=hibernate-commons-annotations, version=3.3.0.GA, type=jar}] has changed (was Dependency {groupId=org.hibernate, artifactId=hibernate-commons-annotations, version=3.3.0.GA, type=jar}).
[INFO] Dependency[Dependency {groupId=commons-collections, artifactId=commons-collections, version=3.2, type=jar}] has changed (was Dependency {groupId=commons-collections, artifactId=commons-collections, version=3.2, type=jar}).
[INFO] Dependency[Dependency {groupId=commons-dbcp, artifactId=commons-dbcp, version=1.1, type=jar}] has changed (was Dependency {groupId=commons-dbcp, artifactId=commons-dbcp, version=1.1, type=jar}).
[INFO] Dependency[Dependency {groupId=hsqldb, artifactId=hsqldb, version=1.8.1.2, type=jar}] has changed (was Dependency {groupId=hsqldb, artifactId=hsqldb, version=1.8.1.2, type=jar}).
[INFO] Processing war project
[INFO] Copying webapp resources[c:\projects\sdc.samples\sdc.samples.htmlunit\src\main\webapp]
[INFO] Webapp assembled in[162 msecs]
[INFO] Building war: c:\projects\sdc.samples\sdc.samples.htmlunit\target\sdc.samples.htmlunit-1.0-SNAPSHOT.war
[INFO] [failsafe:integration-test {execution: integration-test}]
[INFO] Failsafe report directory: c:\projects\sdc.samples\sdc.samples.htmlunit\target\failsafe-reports

-------------------------------------------------------
 T E S T S
-------------------------------------------------------
Running sdc.startupassets.spring25.integrationtest.CodesIntegrationTestCase
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 5.065 sec

Results :

Tests run: 1, Failures: 0, Errors: 0, Skipped: 0

[WARNING] File encoding has not been set, using platform encoding Cp1252, i.e. build is platform dependent!
[INFO] [failsafe:verify {execution: verify}]
[INFO] Failsafe report directory: c:\projects\sdc.samples\sdc.samples.htmlunit\target\failsafe-reports
[WARNING] File encoding has not been set, using platform encoding Cp1252, i.e. build is platform dependent!
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESSFUL
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 9 seconds
[INFO] Finished at: Tue Sep 27 15:39:00 CEST 2011
[INFO] Final Memory: 17M/42M
[INFO] ------------------------------------------------------------------------

The high-level sequence is easy to remember: compile, unit tests, package, integration tests.

Automating server start, stop and application deployment during test execution

That was nice, but it would be nicer if we didn’t have to start and stop the server manually to run the integration tests. If Maven could do that for us automatically when needed, it would be perfect.

Maven actually does, by leveraging the Maven Cargo plug-in (more information here). Cargo is capable of remotely deploy deliverables to a server, or download and install one locally for the test session if we do not have one available. Cargo can also automatically start and stop servers (both remote and local).

To configure Cargo we would need to add the plug-in to the POM and bind the server start and stop actions to the pre-integration-test and post-integration-test phases of the project lifecycle. We can also configure the brand and version of the application server to use, if it is available or should be downloaded and installed locally plus other configuration parameters needed to fine-control the server behavior.

In our example, we will select a Tomcat 7 server downloadable from Apache site. Therefore we add this to the POM <build> section:

            <plugin>
                <groupId>org.codehaus.cargo</groupId>
                <artifactId>cargo-maven2-plugin</artifactId>
                <version>1.1.2</version>
                <configuration>
                    <container>
                        <containerId>tomcat7x</containerId>
                        <zipUrlInstaller>
                            <url>http://archive.apache.org/dist/tomcat/tomcat-7/v7.0.6/bin/apache-tomcat-7.0.6.zip</url>
                        </zipUrlInstaller>
                    </container>
                </configuration>
                <executions>
                    <!-- start server before integration tests -->
                    <execution>
                        <id>start-container</id>
                        <phase>pre-integration-test</phase>
                        <goals>
                            <goal>start</goal>
                        </goals>
                    </execution>
                    <!-- stop server after integration tests -->
                    <execution>
                        <id>stop-container</id>
                        <phase>post-integration-test</phase>
                        <goals>
                            <goal>stop</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>

Now, with this small piece of configuration, when we run the verify goal in Maven we will see how the server is downloaded, extracted, started, the application deployed, integration tests are executed and finally the server is stopped. All without human intervention!

[INFO] Scanning for projects...
[INFO] ------------------------------------------------------------------------
[INFO] Building sdc.samples.htmlunit
[INFO]    task-segment: [verify]
[INFO] ------------------------------------------------------------------------
... usual compile and unit test stuff
[INFO] [war:war {execution: default-war}]
[INFO] Packaging webapp
[INFO] Assembling webapp[sdc.samples.htmlunit] in [c:\projects\sdc.samples\sdc.samples.htmlunit\target\sdc.samples.htmlunit-1.0-SNAPSHOT]
[INFO] Processing war project
[INFO] Copying webapp resources[c:\projects\sdc.samples\sdc.samples.htmlunit\src\main\webapp]
[INFO] Webapp assembled in[886 msecs]
[INFO] Building war: c:\projects\sdc.samples\sdc.samples.htmlunit\target\sdc.samples.htmlunit-1.0-SNAPSHOT.war
[INFO] [cargo:start {execution: start-container}]
[INFO] [stalledLocalDeployer] Deploying [c:\projects\sdc.samples\sdc.samples.htmlunit\target\sdc.samples.htmlunit-1.0-SNAPSHOT.war] to [c:\projects\sdc.samples\sdc.samples.htmlunit\target\cargo\configurations\tomcat7x/webapps]...
[INFO] [talledLocalContainer] Tomcat 7.x starting...
... usual Tomcat start traces
[WARNING] [talledLocalContainer] 28-sep-2011 12:54:02 org.apache.catalina.startup.HostConfig deployWAR
[WARNING] [talledLocalContainer] INFO: Deploying web application archive sdc.samples.htmlunit-1.0-SNAPSHOT.war
[WARNING] [talledLocalContainer] 28-sep-2011 12:54:07 org.apache.catalina.core.ApplicationContext log
[WARNING] [talledLocalContainer] INFO: Initializing Spring FrameworkServlet 'dispatcher'
... more Spring traces
[WARNING] [talledLocalContainer] 28-sep-2011 12:54:08 org.apache.coyote.AbstractProtocolHandler start
[WARNING] [talledLocalContainer] INFO: Starting ProtocolHandler ["http-bio-8080"]
[WARNING] [talledLocalContainer] 28-sep-2011 12:54:08 org.apache.coyote.AbstractProtocolHandler start
[WARNING] [talledLocalContainer] INFO: Starting ProtocolHandler ["ajp-bio-8009"]
[WARNING] [talledLocalContainer] 28-sep-2011 12:54:08 org.apache.catalina.startup.Catalina start
[WARNING] [talledLocalContainer] INFO: Server startup in 6258 ms
[INFO] [talledLocalContainer] Tomcat 7.x started on port [8080]
[INFO] [failsafe:integration-test {execution: integration-test}]
[INFO] Failsafe report directory: c:\projects\sdc.samples\sdc.samples.htmlunit\target\failsafe-reports

-------------------------------------------------------
 T E S T S
-------------------------------------------------------
Running sdc.startupassets.spring25.integrationtest.CodesIntegrationTestCase
... lots of Spring and Hibernate traces
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 6.964 sec

Results :

Tests run: 1, Failures: 0, Errors: 0, Skipped: 0

[WARNING] File encoding has not been set, using platform encoding Cp1252, i.e. build is platform dependent!
[INFO] [cargo:stop {execution: stop-container}]
[INFO] [talledLocalContainer] Tomcat 7.x is stopping...
[WARNING] [talledLocalContainer] 28-sep-2011 12:54:16 org.apache.catalina.core.StandardServer await
[WARNING] [talledLocalContainer] INFO: A valid shutdown command was received via the shutdown port. Stopping the Server instance.
... more Tomcat 7 stop traces
[INFO] [talledLocalContainer] Tomcat 7.x is stopped
[INFO] [failsafe:verify {execution: verify}]
[INFO] Failsafe report directory: c:\projects\sdc.samples\sdc.samples.htmlunit\target\failsafe-reports
[WARNING] File encoding has not been set, using platform encoding Cp1252, i.e. build is platform dependent!
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESSFUL
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 36 seconds
[INFO] Finished at: Wed Sep 28 12:54:22 CEST 2011
[INFO] Final Memory: 19M/64M
[INFO] ------------------------------------------------------------------------

Conclusion

It is easy and straightforward, when you know how, to configure Maven to fully automate integration tests execution for web applications deployed on a server. Although the approach shown here is valid for HtmlUnit based tests, it can be applied as well with other web  integration testing tools.

As briefly explained, Cargo can also be used to manage remote servers, therefore enabling developer-to-production development cycles, also known as continuous delivery or continuous deployment, without the need to invest in expensive tools and environments.

This approach also enables easy measurements of code coverage of integration tests. In the next post in the series we will discuss how to do that with JaCoCo and Sonar. See you there!

Author: deors

senior technology architect in accenture, with a passion for technology related stuff, celtic music and the best sci-fi, among other thousand things!

2 thoughts on “Integrate HtmlUnit Based Tests with Apache Maven and Cargo”

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s