Filed under: J2EE, Java, — Tags: CI, Cargo, Continuous integration, JUnit, TDD, Test automation — Thomas Sundberg — 2010-12-21
We want to perform an integration test of a system using Maven. The application must be deployed on an application server and the application server must be started before we can perform the integration tests. The application should be undeployed and the application server should be terminated after the integration test has been performed no matter what the result of the test was.
How can this be achieved with Maven?
We will start with setting up a Maven structure. We want one Maven module for the integration test so it can be clearly separated from any unit tests. We also want to build an enterprise archive that can be deployed, and we need some application logic. This sums up to four Maven modules organized as below:
root -- product -- business-api -- ear -- ejb -- integration-testThe entire project will be tied together with an aggregation pom
pom.xml
<?xml version="1.0" encoding="UTF-8"?> <project> <modelVersion>4.0.0</modelVersion> <groupId>se.sigma.educational.maven.integration.test</groupId> <artifactId>integration-test-with-maven</artifactId> <version>1.0</version> <packaging>pom</packaging> <modules> <module>product</module> <module>integration-test</module> </modules> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <repositories> <repository> <id>JBOSS</id> <name>JBoss Repository</name> <url>http://repository.jboss.org/nexus/content/groups/public-jboss/</url> </repository> </repositories> <dependencies> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.8.2</version> <scope>test</scope> </dependency> </dependencies> </project>
We will need JBoss specific things in the integration test and in the ejb module. I don't want to specify a JBoss
repository more then once so I do it in a top pom, http://repository.jboss.org/nexus/content/groups/public-jboss
.
JUnit will be used in all modules during the test phase so I define it here.
The product is tied together with another aggregation pom defined as
/product/pom.xml
<?xml version="1.0" encoding="UTF-8"?> <project> <modelVersion>4.0.0</modelVersion> <parent> <groupId>se.sigma.educational.maven.integration.test</groupId> <artifactId>integration-test-with-maven</artifactId> <version>1.0</version> </parent> <artifactId>product</artifactId> <packaging>pom</packaging> <modules> <module>business-api</module> <module>ear</module> <module>ejb</module> </modules> </project>
With all project preparations done, it's time to build the product.
The first thing I define is the business api. This will be the api that all the services will understand. I define it in a module of it's own since at least two other modules will depend on it. Both the ejb implementation and the integration test need to be able to implement and use the same interface.
Defining the business api in a separate module is the Maven way, rather than using the configuration property
generateClient
available in the ejb plugin.
In order to remove all complicated logic, I will create a simple Hello World. It will be sufficient and give me a chance to focus on the real problem.
The business api is defined as:
/product/business-api/src/main/java/se/sigma/educational/HelloWorld.java
package se.sigma.educational; public interface HelloWorld { String sayHello(String name); }
There is really nothing to it, it's a trivial java interface.
The Maven pom used to build it:
/product/business-api/pom.xml
<?xml version="1.0" encoding="UTF-8"?> <project> <modelVersion>4.0.0</modelVersion> <parent> <groupId>se.sigma.educational.maven.integration.test</groupId> <artifactId>product</artifactId> <version>1.0</version> </parent> <artifactId>business-api</artifactId> <packaging>jar</packaging> </project>
The service implementing the business api above can be tested as:
/product/ejb/src/test/java/se/sigma/educational/HelloWorldBeanTest.java
package se.sigma.educational; import org.junit.Test; import static org.hamcrest.core.Is.is; import static org.junit.Assert.assertThat; public class HelloWorldBeanTest { @Test public void sayHello() { HelloWorld helloWorld = new HelloWorldBean(); String name = "Fridolf"; String expected = "Hi " + name; String actual = helloWorld.sayHello(name); assertThat(actual, is(expected)); } }
The trivial implementation is:
/product/ejb/src/main/java/se/sigma/educational/HelloWorldBean.java
package se.sigma.educational; import org.jboss.ejb3.annotation.RemoteBinding; import javax.ejb.Remote; import javax.ejb.Stateless; @Stateless @Remote(HelloWorld.class) @RemoteBinding(jndiBinding = "HelloWorldJNDIName") public class HelloWorldBean implements HelloWorld { public String sayHello(String name) { return "Hi " + name; } }
The bean is defined as a @Stateless
bean, it doesn't have any instance variables.
It is also defined as a @Remote(HelloWorld.class)
which means that it implements the HelloWorld
interface as a remote bean.
Finally it is defined to have a jndiBinding @RemoteBinding(jndiBinding = "HelloWorldJNDIName")
, that is
the name it will be available under in the application server.
The pom used to build the ejb looks like this:
/product/ejb/pom.xml
<?xml version="1.0" encoding="UTF-8"?> <project> <modelVersion>4.0.0</modelVersion> <parent> <groupId>se.sigma.educational.maven.integration.test</groupId> <artifactId>product</artifactId> <version>1.0</version> </parent> <artifactId>ejb</artifactId> <packaging>ejb</packaging> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-ejb-plugin</artifactId> <version>2.3</version> <configuration> <ejbVersion>3.1</ejbVersion> </configuration> </plugin> </plugins> </build> <dependencies> <dependency> <groupId>javax</groupId> <artifactId>javaee-api</artifactId> <version>6.0</version> <scope>provided</scope> </dependency> <dependency> <groupId>org.jboss.ejb3</groupId> <artifactId>jboss-ejb3-ext-api</artifactId> <version>1.0.0</version> </dependency> <dependency> <groupId>se.sigma.educational.maven.integration.test</groupId> <artifactId>business-api</artifactId> <version>1.0</version> </dependency> </dependencies> </project>
The javaee-api
is needed so we can get access to the ejb annotations during compile time. It will be
provided during execution by the application server.
To be able to deploy the ejb as a part of a ear we need to package it. The packaging is done using a Maven module. The pom that performs the build looks like this:
/product/ear/pom.xml
<?xml version="1.0" encoding="UTF-8"?> <project> <modelVersion>4.0.0</modelVersion> <parent> <groupId>se.sigma.educational.maven.integration.test</groupId> <artifactId>product</artifactId> <version>1.0</version> </parent> <artifactId>ear</artifactId> <packaging>ear</packaging> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-ear-plugin</artifactId> <version>2.4.2</version> <configuration> <archive> <manifest> <addClasspath>true</addClasspath> </manifest> </archive> <modules> <jarModule> <groupId>se.sigma.educational.maven.integration.test</groupId> <artifactId>ejb</artifactId> <includeInApplicationXml>true</includeInApplicationXml> </jarModule> <jarModule> <groupId>se.sigma.educational.maven.integration.test</groupId> <artifactId>business-api</artifactId> <includeInApplicationXml>true</includeInApplicationXml> </jarModule> </modules> </configuration> </plugin> </plugins> </build> <dependencies> <dependency> <groupId>se.sigma.educational.maven.integration.test</groupId> <artifactId>ejb</artifactId> <version>1.0</version> </dependency> <dependency> <groupId>se.sigma.educational.maven.integration.test</groupId> <artifactId>business-api</artifactId> <version>1.0</version> </dependency> </dependencies> </project>
The product is done. Or rather, it ought to be done. We haven't tested it in an application server yet so we can't be sure. That's next.
So, we have a business interface, we have an implementation and we can package it as an ear. All we are missing is an integration test that deploys the ear, launches a JBoss, runs a set of tests and finally tears down the test bench.
The integration test looks like this:
/integration-test/src/test/java/se/sigma/educational/integration/test/HelloWorldIntegrationTest.java
package se.sigma.educational.integration.test; import org.junit.Test; import se.sigma.educational.HelloWorld; import javax.naming.Context; import javax.naming.InitialContext; import javax.naming.NamingException; import java.util.Properties; import static org.hamcrest.core.Is.is; import static org.junit.Assert.assertThat; public class HelloWorldIntegrationTest { @Test public void verifyHelloWorld() throws NamingException { Properties properties = new Properties(); properties.put("java.naming.factory.initial", "org.jnp.interfaces.NamingContextFactory"); properties.put("java.naming.factory.url.pkgs", "=org.jboss.naming:org.jnp.interfaces"); properties.put("java.naming.provider.url", "localhost:1099"); Context ctx = new InitialContext(properties); HelloWorld reflector = (HelloWorld) ctx.lookup("HelloWorldJNDIName"); String name = "Thomas"; String expected = "Hi " + name; String actual = reflector.sayHello(name); assertThat(actual, is(expected)); } }
It's really nothing special here. A remote session bean is located, called and the answer is asserted.
The magic is in the Maven project that downloads a JBoss, deploys the ear, starts JBoss, runs the integration tests and finally tear up the test bench. This is done in a Maven project that looks like this:
/example/integration-test/pom.xml
<?xml version="1.0" encoding="UTF-8"?> <project> <modelVersion>4.0.0</modelVersion> <parent> <groupId>se.sigma.educational.maven.integration.test</groupId> <artifactId>integration-test-with-maven</artifactId> <version>1.0</version> </parent> <artifactId>integration-test</artifactId> <packaging>jar</packaging> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> <version>2.6</version> <configuration> <excludes> <exclude>**</exclude> </excludes> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-failsafe-plugin</artifactId> <version>2.6</version> <configuration> <includes> <include>**/integration/**</include> </includes> </configuration> <executions> <execution> <id>integration-test</id> <phase>integration-test</phase> <goals> <goal>integration-test</goal> <goal>verify</goal> </goals> </execution> </executions> </plugin> <plugin> <groupId>org.codehaus.cargo</groupId> <artifactId>cargo-maven2-plugin</artifactId> <version>1.0.5</version> <configuration> <container> <append>false</append> <containerId>jboss51x</containerId> <log>${project.build.directory}/logs/jboss51x.log</log> <output>${project.build.directory}/logs/jboss51x.out</output> <timeout>60000</timeout> <zipUrlInstaller> <installDir>${project.build.directory}/JBoss</installDir> <url>http://downloads.sourceforge.net/project/jboss/JBoss/JBoss-5.1.0.GA/jboss-5.1.0.GA-jdk6.zip</url> </zipUrlInstaller> </container> <configuration> <type>standalone</type> <home>${project.build.directory}/integration-test</home> <properties> <cargo.servlet.port>8080</cargo.servlet.port> <cargo.jboss.configuration>default</cargo.jboss.configuration> <cargo.rmi.port>1099</cargo.rmi.port> <cargo.logging>high</cargo.logging> </properties> <deployables> <deployable> <groupId>se.sigma.educational.maven.integration.test</groupId> <artifactId>ear</artifactId> <type>ear</type> </deployable> </deployables> </configuration> <wait>false</wait> </configuration> <executions> <execution> <id>start-container</id> <phase>pre-integration-test</phase> <goals> <goal>start</goal> </goals> </execution> <execution> <id>stop-container</id> <phase>post-integration-test</phase> <goals> <goal>stop</goal> </goals> </execution> </executions> </plugin> </plugins> </build> <dependencies> <dependency> <groupId>org.jboss.jbossas</groupId> <artifactId>jboss-as-client</artifactId> <version>5.1.0.GA</version> <type>pom.sha1.audit.json.sha1.audit.json</type> </dependency> <dependency> <groupId>se.sigma.educational.maven.integration.test</groupId> <artifactId>business-api</artifactId> <version>1.0</version> </dependency> <dependency> <groupId>se.sigma.educational.maven.integration.test</groupId> <artifactId>ear</artifactId> <version>1.0</version> <type>ear</type> </dependency> </dependencies> </project>
The integration test pom is where the magic happens.
There are a few things to notice in the integration-test pom.
First, we need access to an implementation of org.jnp.interfaces.NamingContextFactory
when the test are
executed. We will get the access to it if we add a dependency to jboss-as-client
.
The next thing to notice is that I make sure that no test are executed by the surefire plugin. This is done by excluding everything in the default configuration.
Instead, we want to execute the tests using the failsafe plugin. Include everything in the package
integration
and execute the goals integration-test
and verify
in the Maven
life cycle phase integration-test
.
The most important configuration is the one done for the cargo plugin. First I start with defining that I want a
JBoss 5.1 application server by defining the containerId
to jboss51x
Note that you must set either the home
element or define a zipUrlInstaller
element. I have defined a zip url installer so a JBoss will be downloaded and installed. The
reason for this is simple, I want to make sure that I perform the integration tests on a vanilla JBoss
installation. Any configuration that I want to have done should be done in the built artifact. I want to be
able to drop the ear on any JBoss and it should run without any configuration.
Then I define the container type to be a standalone
which implies that I will always get a standalone
installation of a JBoss application server. The type standalone also means that the installation always is deleted
before a new execution. I define the home
element to the Maven target directory.
This means that the server is installed in ./target/
.
Then I list the deployables that will be deployed. In this case there will only be one deployable.
Starting the application server is done in the pre-integration-test
phase. Stopping the application
server is done in the post-integration-test
.
Run the integration test by executing
mvn clean install
This may take a while, there may be a lot of dependencies to download.
Done!