Integration Testing with HTTP, HTTPS and Maven
Earlier this week, I was tasked with getting automated integration tests working in my project at Overstock.com. By automated, I mean that ability to run "mvn install" and have the following process cycled through:
- Start a container
- Deploy the application
- Run all integration tests
- Stop the container
Since it makes sense for integration tests to run in Maven's integration-test phase, I first configured the maven-surefire-plugin to skip tests in the test phase and execute them in the integration-test phase. I used the <id>default-phase</id> syntax to override the plugins' usual behavior.
<plugin> <artifactId>maven-surefire-plugin</artifactId> <executions> <execution> <id>default-test</id> <configuration> <excludes> <exclude>**/*Test*.java</exclude> </excludes> </configuration> </execution> <execution> <id>default-integration-test</id> <phase>integration-test</phase> <goals> <goal>test</goal> </goals> <configuration> <includes> <include>**/*Test.java</include> </includes> <excludes> <exclude>none</exclude> <exclude>**/TestCase.java</exclude> </excludes> </configuration> </execution> </executions> </plugin>
After I had this working, I moved onto getting the container started and stopped properly. In the past, I've done this using Cargo and it's always worked well for me. Apart from the usual setup I use in AppFuse archetypes (example pom.xml), I added a couple additional items:
- Added <timeout>180000</timeout> so the container would wait up to 3 minutes for the WAR to deploy.
- In configuration/properties, specified <context.path>ROOT</context.path> so the app would deploy at the / context path.
- In configuration/properties, specified <cargo.protocol>https</cargo.protocol> since many existing unit tests made requests to secure resources.
I started by using Cargo with Tomcat and had to create certificate keystore in order to get Tomcat to start with SSL enabled. After getting it to start, I found the tests failed with the following errors in the logs:
javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target at com.sun.net.ssl.internal.ssl.Alerts.getSSLException(Alerts.java:174) at com.sun.net.ssl.internal.ssl.SSLSocketImpl.fatal(SSLSocketImpl.java:1649)
Co-workers told me this was easily solved by adding my 'untrusted' cert to my JVM keystore. Once all this was working, I thought I was good to go, but found that some tests were still failing. The failures turned out to be because they were talking to http and https was the only protocol enabled. After doing some research, I discovered that Cargo doesn't support starting on both http and https ports.
So back to the drawing board I went. I ended up turning to the maven-jetty-plugin and the tomcat-maven-plugin to get the functionality I was looking for. I also automated the certificate keystore generation using the keytool-maven-plugin. Below is the extremely-verbose 95-line profiles section of my pom.xml that allows either container to be used.
Sidenote: I wonder how this same setup would look using Gradle?
<profiles> <profile> <id>jetty</id> <activation> <activeByDefault>true</activeByDefault> </activation> <build> <plugins> <plugin> <groupId>org.mortbay.jetty</groupId> <artifactId>maven-jetty-plugin</artifactId> <version>6.1.26</version> <configuration> <contextPath>/</contextPath> <connectors> <connector implementation="org.mortbay.jetty.nio.SelectChannelConnector"> <!-- forwarded == true interprets x-forwarded-* headers --> <!-- http://docs.codehaus.org/display/JETTY/Configuring+mod_proxy --> <forwarded>true</forwarded> <port>8080</port> <maxIdleTime>60000</maxIdleTime> </connector> <connector implementation="org.mortbay.jetty.security.SslSocketConnector"> <forwarded>true</forwarded> <port>8443</port> <maxIdleTime>60000</maxIdleTime> <keystore>${project.build.directory}/ssl.keystore</keystore> <password>overstock</password> <keyPassword>overstock</keyPassword> </connector> </connectors> <stopKey>overstock</stopKey> <stopPort>9999</stopPort> </configuration> <executions> <execution> <id>start-jetty</id> <phase>pre-integration-test</phase> <goals> <goal>run-war</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> </plugins> </build> </profile> <profile> <id>tomcat</id> <build> <plugins> <plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>tomcat-maven-plugin</artifactId> <version>1.1</version> <configuration> <addContextWarDependencies>true</addContextWarDependencies> <fork>true</fork> <path>/</path> <port>8080</port> <httpsPort>8443</httpsPort> <keystoreFile>${project.build.directory}/ssl.keystore</keystoreFile> <keystorePass>overstock</keystorePass> </configuration> <executions> <execution> <id>start-tomcat</id> <phase>pre-integration-test</phase> <goals> <goal>run-war</goal> </goals> </execution> <execution> <id>stop-tomcat</id> <phase>post-integration-test</phase> <goals> <goal>shutdown</goal> </goals> </execution> </executions> </plugin> </plugins> </build> </profile> </profiles>
With this setup in place, I was able to automate running our integration tests by simply typing "mvn install" (for Jetty) or "mvn install -Ptomcat" (for Tomcat). For running in Hudson, it's possible I'll have to further enhance things to randomize the port and pass that into tests as a system property. The build-helper-maven-plugin and its reserve-network-port goal is a nice way to do this. Note: if you want to run more than one instance of Tomcat at a time, you might have to randomize the ajp and rmi ports to avoid collisions.
The final thing I encountered was our app didn't shutdown gracefully. Luckily, this was fixed in a newer version of our core framework and upgrading fixed the problem. Here's the explanation from an architect on the core framework team.
The hanging problem was caused by the way the framework internally aggregated statistics related to database connection usage and page response times. The aggregation runs on a separate thread but not as a daemon thread. Previously, the aggregation threads weren't being terminated on shutdown so the JVM would hang waiting for them to finish. In the new frameworks, the aggregation threads are terminated on shutdown.
Hopefully this post helps you test your secure and unsecure applications at the same time. At the same time, I'm hoping it motivates the Cargo developers to add simultaneous http and https support.
Update: In the comments, Ron Piterman recommended I use the Maven Failsafe Plugin because its designed to run integration tests while Surefire Plugin is for unit tests. I changed my configuration to the following and everything still passes. Thanks Ron!
<plugin> <artifactId>maven-surefire-plugin</artifactId> <version>2.7.2</version> <configuration> <skipTests>true</skipTests> </configuration> </plugin> <plugin> <artifactId>maven-failsafe-plugin</artifactId> <version>2.7.2</version> <configuration> <includes> <include>**/*Test.java</include> </includes> <excludes> <exclude>**/TestCase.java</exclude> </excludes> </configuration> <executions> <execution> <id>integration-test</id> <phase>integration-test</phase> <goals> <goal>integration-test</goal> </goals> </execution> <execution> <id>verify</id> <phase>verify</phase> <goals> <goal>verify</goal> </goals> </execution> </executions> </plugin>
Update 2: In addition to application changes to solve hanging issues, I also had to change my Jetty Plugin configuration to use a different SSL connector implementation. This also required adding the jetty-sslengine dependency, which has been renamed to jetty-ssl for Jetty 7.
<connector implementation="org.mortbay.jetty.security.SslSelectChannelConnector"> ... <dependencies> <dependency> <groupId>org.mortbay.jetty</groupId> <artifactId>jetty-sslengine</artifactId> <version>6.1.26</version> </dependency> </dependencies>
Great article !!
Thank you.
What is the <exclude>none</exclude> for ?
Posted by Dragisa Krsmanovic on February 11, 2011 at 11:27 PM MST #
Hi,
Good story, I had to deal with maven selenium tests too, but always using http protocol, the maven setup becomes verbose adding those profiles. I got the same question about Gradle, things like adding random numbers for ports should be easier in Gradle.
Some projects like hibernate i think are using Gradle, but maven still the standard.
Posted by Ricardo Espergue on February 11, 2011 at 11:45 PM MST #
Posted by Ron Piterman on February 12, 2011 at 08:30 AM MST #
Posted by paul.mckenzie on February 15, 2011 at 02:08 PM MST #
@Dragisa - It might not be necessary, but seems to be documented in Maven and Integration Testing. In the past, I've found it necessary when you want to override exclusions in a phase's configuration
@Ron - thanks for the tip. I've changed to the failsafe plugin and updated this post.
@Paul - Did you get a satisfactory answer from your question on Stack Overflow?
Posted by Matt Raible on February 16, 2011 at 11:40 PM MST #
Posted by Raible Designs on February 24, 2011 at 06:04 AM MST #
hi.
thanks for this post. Only simple question... but maybe not so simple answer : how do tell the tomcat maven plugin to include the src/test/resources during the integration test phase ?
without using a bbom profile trick ...
Posted by yann on April 22, 2011 at 02:12 PM MDT #
Hi, I liked your article a lot and because of its technical nature thought you might be interested in this.
http://johndobie.blogspot.com/2011/06/seperating-maven-unit-integration-tests.html
Its a nice tidy way of splitting the tests that you deploy to your server.
All the best.
Posted by John Dobie on June 29, 2011 at 03:32 PM MDT #
Posted by Matt Raible on June 29, 2011 at 05:11 PM MDT #