EE integration testing using Arquillian

Writing tests in a EE environment is a challenge. Compared to test support in spring framework there is some work.

Fortunately though, thanks to Arquillian, there is hope.

A lot of the complications are related with the difference in environment. In spring framework, you include all the libraries in your application, which means everything is readily available. In EE you need a container which provides facilities which are not directly included. Arquillian fixes this by allowing you to run your tests inside the container.

An Arquillian tests is almost the same as a non-Arquillian test, except that you also have to build the deployment package to run the test. While the general recommendation seems that you make such package as small as possible, I believe this can be a challenge as you do need all dependencies for these minimal classes needed for the test. In my mind it seems best to simply deploy the full package under development (jar or war) to run your tests.

The situation in Spring is similar. While it is easy to have a different context for each test, in practice it is better to reduce the number of context for running tests. Partly to make sure there are no dangling dependencies and partly to reduce the run time of your test suite.

In Arquillian the complexity is again slightly increased as you also need to include all project dependencies (as in “other” jars). You have to specify the project classes to include and the location of all dependencies.

In my application I use a base class for all integration tests. The dependency details are picked up from the maven pom (including sub-dependencies – unfourtunately just including all dependencies at once did not work). For the project classes themselves, I use a helper to simply include all classes of a package include the sub-packages which are found in the source. You also need to make sure all required resources are managed. Here again they are scanned from the source.

@RunWith(Arquillian.class)
public abstract class AbstractIT {
 
    private static final String SOURCES_DIR = "src/main/java";
    private static final String RESOURCES_DIR = "src/main/resources";
 
    @Deployment
    public static Archive<?> createDeployment() throws Exception {
        String pathPrefix = ""; // trick to make this work from both the main as
        // module root directory
        File currentDir = new File(".");
        if (!currentDir.getCanonicalPath().endsWith("back-impl")) {
            pathPrefix = "back-impl/";
        }
        PomEquippedResolveStage pom = Maven.resolver().loadPomFromFile(pathPrefix + "pom.xml");
        File[] commonsLang = pom.resolve("org.apache.commons:commons-lang3").withTransitivity().asFile();
        File[] appApi = pom.resolve("be.myapp:myapp-api").withTransitivity().asFile();
        File[] jTransfoCore = pom.resolve("org.jtransfo:jtransfo-core").withTransitivity().asFile();
        File[] jTransfoCdi = pom.resolve("org.jtransfo:jtransfo-cdi").withTransitivity().asFile();
        File[] queryDsl = pom.resolve("com.mysema.querydsl:querydsl-jpa").withTransitivity().asFile();
        File[] flyway = pom.resolve("com.googlecode.flyway:flyway-core").withTransitivity().asFile();
        File[] festAssert = pom.resolve("org.easytesting:fest-assert").withTransitivity().asFile();
        File[] httpclient = pom.resolve("org.apache.httpcomponents:httpclient").withTransitivity().asFile();
        File[] deltaspikeCore = pom.resolve("org.apache.deltaspike.core:deltaspike-core-impl").withTransitivity().asFile();
        File[] deltaspikeData = pom.resolve("org.apache.deltaspike.modules:deltaspike-data-module-impl").withTransitivity().asFile();
        WebArchive war = ShrinkWrap.create(WebArchive.class, "test.war").
                addAsLibraries(commonsLang).
                addAsLibraries(appApi).
                addAsLibraries(jTransfoCore).
                addAsLibraries(jTransfoCdi).
                addAsLibraries(queryDsl).
                addAsLibraries(flyway).
                addAsLibraries(festAssert).
                addAsLibraries(httpclient).
                addAsLibraries(deltaspikeCore).
                addAsLibraries(deltaspikeData).
                addAsResource("META-INF/persistence.xml").
                addAsResource("META-INF/beans.xml");
        addAllPackages(war, "be.fluxtock.back.impl", new File(pathPrefix + SOURCES_DIR + "/be/fluxtock/back/impl"));
        addAllResources(war, pathPrefix + RESOURCES_DIR);
        return war;
    }
 
    /**
     * Add all packages starting with given prefix from given path.
     *
     * @param war war archive to add packages to
     * @param prefix base package
     * @param dir directory for the base package
     */
    private static void addAllPackages(WebArchive war, String prefix, File dir) {
        war.addPackage(prefix);
        for (File file : dir.listFiles(File::isDirectory)) {
            addAllPackages(war, prefix + "." + file.getName(), file);
        }
    }
 
    /**
     * Add all resources from the given directory, recursively. Only adds
     * subdirectories when they start with a lower case letter
     *
     * @param war war archive to add packages to
     * @param directory directory with resources to add
     */
    private static void addAllResources(WebArchive war, String directory) {
        for (File file : new File(directory).listFiles(pathname -> pathname.isFile() || Character.isLowerCase(pathname.getName().charAt(0)))) {
            addAllResources(war, "", file);
        }
    }
 
    private static void addAllResources(WebArchive war, String prefix, File dir) {
        if (dir.isDirectory()) {
            prefix += dir.getName() + "/";
            for (File file : dir.listFiles()) {
                addAllResources(war, prefix, file);
            }
        } else {
            war.addAsResource(dir, prefix + dir.getName());
        }
    }
 
}

To allow the dependencies to be resolved from the pom, you need an extra dependency.

<dependency>
    <groupId>org.jboss.shrinkwrap.resolver</groupId>
    <artifactId>shrinkwrap-resolver-impl-maven</artifactId>
    <version>2.0.0</version>
    <scope>test</scope>
</dependency>

When using a database to run your tests, you also need to make sure your database is not polluted by your tests. Something similar to Spring’s AbstractTransactionalJUnit4SpringContextTests can be built quite easily.

/**
 * Transactional integration test. Transaction is rolled back at the end.
 */
public abstract class AbstractRollbackIT extends AbstractIT {
 
    @PersistenceContext
    EntityManager em;
 
    @Inject
    UserTransaction utx;
 
    @Before
    public void setUp() throws Exception {
        utx.begin();
        em.joinTransaction();
    }
 
    @After
    public void tearDown() throws Exception {
        utx.rollback();
    }
 
}

With these base classes test are running fine. Unfortunately it does seem that a new deployment is created for each test. It would be nice (and faster) if tests which share the same deployment could also reuse that deployment – reducing the number of times the deployment is started in the container. Fortunately – compared to context initialization in spring framework – this initialization is very fast, but it would probably make a big difference when there are many integration tests.

3 Comments

  1. rmpestano says:

    Hi there,

    you can avoid multiple deployments by injecting tests in a common deployment, this works for integration tests(testable=true).
    You can see an exaple here:https://github.com/conventions/archetype/blob/master/src/test/java/org/conventions/archetype/test/it/UserIT.java

    RoleIT leverages UserIT deployment, this is a bit hack but works I think arquillian 2.x will have the notion of TestSuite and share deployments.

  2. Bartosz Majsak says:

    You also don’t need to handle transactions on you own – there is an extension for that :)
    https://github.com/arquillian/arquillian-extension-transaction

Leave a Reply

Your email address will not be published. Required fields are marked *

*

question razz sad evil exclaim smile redface biggrin surprised eek confused cool lol mad twisted rolleyes wink idea arrow neutral cry mrgreen

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>