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>