Blog

Middleware integration testing with JUnit, Maven and VMware, part 2 (of 3)

14 Dec, 2009
Xebia Background Header Wave

Last week I wrote about the approach we use at XebiaLabs to test the integrations with the different middleware products our Java EE deployment automation product Deployit supports.

The blog finished with the promise that I would discuss how to test that an application can really use the configurations that our middleware integrations (a.k.a. steps) create. But before we delve into that, let us first answer the question as to why we need this. If the code can configure a datasource without the application server, it must be OK for an application to use it, right? Well, not always. While WebSphere and WebLogic contain some functionality to test the connection to the database and thereby verify whether the datasource has been configured correctly, this functionality is not available for other configurations such as JMS settings. And JBoss has no such functionality at all. So the question is: how can we prove that an application can really work with the configurations created by our steps?

Enter the big ol’ test application

The first thing we tried was to write a test application that exercises all the functionality of a Java EE application server. Something like the Java EE Pet Store application. So we started up Eclipse WTP and wrote a number of servlets/JSPs/Spring controllers that do stuff like query a table, put a message on a queue and retrieve, and so forth. We packaged that application into an EAR and deployed it into our target application server and checked whether it an correctly.

We could make this part of our continous integration by writing a JUnit test that deploys this application on each of the application servers and then exercises the application by requesting different URLs. But there are a few problems with this solution:

  1. Every time you modify the test application you have to rebuild it and put the resulting EAR file into the test resources directory (src/test/resources/...) for the middleware intregation project.
  2. There is only one test application for all the three supported application servers: IBM WebSphere Application Server, Oracle WebLogic Server and JBoss Application Server. But the tests you might want to do for each are slightly different. For example, WebSphere offers queues to a built-in Service Integration Bus, to WebSphere MQ and to regular JMS providers. One could concievably make three separete tests applications but that only makes matters more complicated.

The basic problem here is that this test application and the other part of the test (the part described in part 1 of this series) are stored in different places like this. Wouldn’t things work a lot better if these bits of code were together?

Actually, wouldn’t it be nice if we could create these test applications on the fly? With just that little bit of test code we want to run on the application server in its Java EE context? Yes? Well I hoped you’d agree, because that is exactly what the OnTheFly library we wrote does. 😉

Creating JAR files on the fly

The first thing we needed to do is to make sure we can create a JAR file on the fly. If we have that in place we can create WAR files and EAR files. This is code for our JarOnTheFly class:

import java.io.*;
import java.util.*;
import org.apache.commons.io.IOUtils;
import org.springframework.core.io.Resource;
public class JarOnTheFly {
    private Map files = new HashMap();
    public void addFile(String filename, Resource resource) {
        files.put(filename, resource);
    }
    public void write(File jarFile) throws IOException {
        FileOutputStream jarFileOut = new FileOutputStream(jarFile);
        try {
            JarOutputStream jarOut = new JarOutputStream(jarFileOut);
            try {
                for (Map.Entry eachFile : files.entrySet()) {
                    String filename = eachFile.getKey();
                    Resource resource = eachFile.getValue();
                    jarOut.putNextEntry(new JarEntry(filename));
                    InputStream resourceIn = resource.getInputStream();
                    try {
                        IOUtils.copy(resourceIn, jarOut);
                    } finally {
                        IOUtils.closeQuietly(resourceIn);
                    }
                    jarOut.closeEntry();
                }
            } finally {
                IOUtils.closeQuietly(jarOut);
            }
        } finally {
            IOUtils.closeQuietly(jarFileOut);
        }
    }
    protected File writeToTemporaryFile(String prefix, String suffix) throws IOException {
        File tempJarFile = File.createTempFile(prefix, suffix);
        write(tempJarFile);
        return tempJarFile;
    }
}

You can create an instance of this class, add files to it using the addFile method and then write it to a specific file or to a temporary file. Because you can pass in any class that implements the Spring Resource interface you can put any kind of content in the JAR file: regular files, files from the classpath, in-memory byte arrays, and even input streams.

Creating WAR files on the fly

The WarOnTheFly class builds on the JarOnTheFly class to add a number of WAR specific features. First of all, a minimal WEB-INF/web.xml can be created. If you invoke the writeToTemporaryFile method this will be done automatically and the extension will automatically be set to .war.

Secondly and most importantly, you can add any class in your current classpath to the WAR file as a servlet. The simple name of the class is added to the WEB-INF/web.xml as the name of the servlet.

While it is also possible to add test JSPs to this WAR file, the ability to write your test code as a servlet is what makes this whole approach so nice. The test servlet code can be placed right next to the rest of the test code so it is always in your face while you are working on the tests. A limitation of the current implementation is that your test servlet should be contained in exactly one test class; any helper classes or inner classes will not be copied to the WAR file.

import java.io.*;
import java.util.*;
import org.apache.velocity.VelocityContext;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.core.io.ClassPathResource;
public class WarOnTheFly extends JarOnTheFly {
    private String name;
    private Map servlets;
    public WarOnTheFly(String name) {
        this.name = name;
        this.servlets = new HashMap();
    }
    public void addServlet(Class servletClass) {
        String servletName = servletClass.getSimpleName();
        String servletClassFilename = servletClass.getName().replace('.', '/') + ".class";
        addFile("WEB-INF/classes/" + servletClassFilename,
            new ClassPathResource(servletClassFilename));
        servlets.put(servletName, servletClass.getName());
    }
    public void addWebXml() {
        VelocityContext context = new VelocityContext();
        context.put("name", name);
        context.put("servlets", servlets);
        String webxml = evaluateTemplate(context,
            "com/xebialabs/deployit/test/support/onthefly/web.xml.vm");
        addFile("WEB-INF/web.xml", new ByteArrayResource(webxml.getBytes()));
    }
    public File writeToTemporaryFile() throws IOException {
        addWebXml();
        return writeToTemporaryFile(name, ".war");
    }
    public String getName() {
        return name;
    }
}

The evaluateTemplate method that is invoked from the addWebXml method is a static utility method that reads a Velocity template from the classpath location specified and evaluates it using the specified context. The web.xml.vm looks like this:


    ${name}
#foreach( $key in $servlets.keySet() )

        $key
        $!servlets.get($key)

        $key
        /$key

#end

Creating EAR files on the fly

To not get bitten by the fact that each application server requires you to specify the context root for a WAR file in a different way we need to wrap the WAR file in an EAR file. That is what the EarOnTheFly class does. The implementation should not come as a big surprise after you’ve seen the JarOnTheFly and WarOnTheFly classes:

import java.io.*;
import java.util.*;
import org.apache.velocity.VelocityContext;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
public class EarOnTheFly extends JarOnTheFly {
    private String name;
    private Map wars;
    public EarOnTheFly(String name) {
        this.name = name;
        this.wars = new HashMap();
    }
    public void addWarOnTheFly(WarOnTheFly wotf) throws IOException {
        String warFilename = wotf.getName() + ".war";
        Resource warFile = new FileSystemResource(wotf.writeToTemporaryFile());
        addFile(warFilename, warFile);
        wars.put(wotf.getName(), warFilename);
    }
    private void addApplicationXml() {
        VelocityContext context = new VelocityContext();
        context.put("name", name);
        context.put("wars", wars);
        String webxml = evaluateTemplate(context,
            "com/xebialabs/deployit/test/support/onthefly/application.xml.vm");
        addFile("META-INF/application.xml", new ByteArrayResource(webxml.getBytes()));
    }
    public File writeToTemporaryFile() throws IOException {
        addApplicationXml();
        return writeToTemporaryFile("itest", ".ear");
    }
    public String getName() {
        return name;
    }
}

For completeness here are the contents of the application.xml.vm Velocity template:


    ${name}

#foreach( $key in $wars.keySet() )

            $!wars.get($key)
            $key

#end

The test servlet

So how do we use these classes? We start by writing our test servlet. When invoked the test servlet should exercise the Java EE configuration under test and finally print a specific message when the test succeeds. If something goes wrong an error message can be printed or an exception can be thrown. The following code is the servlet we use to test our data source steps:

public class DataSourceStepsItestServlet extends HttpServlet {
    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
        throws ServletException, IOException
    {
        try {
            InitialContext ic = new InitialContext();
            DataSource ds = (DataSource) ic.lookup("jdbc/itest");
            Connection conn = ds.getConnection();
            try {
                Statement stmt = conn.createStatement();
                try {
                    stmt.executeUpdate("CREATE TABLE TEST_TABLE " +
                        "(THEVALUE VARCHAR(16) )");
                } catch (SQLException ignore) {
                }
                String expectedValue = Integer.toString(new Random().nextInt());
                stmt.executeUpdate("INSERT INTO TEST_TABLE (THEVALUE) " +
                    "VALUES ( \'" + expectedValue + "\' ) ");
                String sql = "SELECT * FROM TEST_TABLE";
                boolean found = false;
                ResultSet rs = stmt.executeQuery(sql);
                try {
                    while (rs.next()) {
                        String returnValue = rs.getString(1);
                        if (!found && returnValue.equals(expectedValue)) {
                            found = true;
                        }
                    }
                } finally {
                    rs.close();
                }
                if (!found) {
                    throw new RuntimeException("INSERTED \"" + expectedValue +
                        " into table TEST_TABLE but could not get it back");
                }
                resp.setContentType("text/plain");
                PrintWriter out = resp.getWriter();
                out.println("The middleware integration test has passed!");
            } finally {
                conn.close();
            }
        } catch (NamingException exc) {
            throw new ServletException(exc);
        } catch (SQLException exc) {
            throw new ServletException(exc);
        }
    }
}

First a test table is created, then a random string is stored in it and finally the contents of the table are read to see whether the table contains that random string. If that works we can be pretty sure that the JDBC setup is correct!

Deploying the test servlet

The next step is to deploy the test servlet to the application server. So we wrap it in a WAR file which gets wrapped in an EAR file (see the code below) and then we use some application server specific code to deploy that EAR file (not shown).

public static EarOnTheFly createItestEarOnTheFly(Class itestServletClass)
        throws IOException
{
    WarOnTheFly wotf = new WarOnTheFly("itest");
    wotf.addServlet(itestServletClass);
    EarOnTheFly eotf = new EarOnTheFly("itest");
    eotf.addWarOnTheFly(wotf);
}

Running the test servlet

Now that our test servlet is deployed on an application server, we have to invoke it and check that it runs successfully. To that end we use the HttpUnit framework because it provides a convenient way to invoke URLs and check the response that comes back.

public static void assertItestServlet(Host serverHost, int port,
        Class itestServletClass) throws IOException, SAXException
{
    String url = "https://" + serverHost.getAddress() + ":" + port + "/" +
        ITEST_WAR_NAME + "/" + itestServletClass.getSimpleName();
    WebConversation wc = new WebConversation();
    WebRequest req = new GetMethodWebRequest(url);
    WebResponse resp = wc.getResponse(req);
    String responseText = resp.getText();
    assertTrue(responseText.contains("The middleware integration test has passed!"));
}

Putting it all together

Finally we have to add these application server-side tests to the middleware integrations tests as I’ve described them in the previous blog. The exact details depends on the application server under test but such a test method should look something like this one from the Deployit’s JBoss plugin:

@Test
public void testDeploymentOfOneDataSourceToExistingServer() throws IOException, SAXException {
    dataSource = new JbossasDataSource();
    dataSource.setJndiName("jdbc/itest");
    dataSource.setDriverClass("org.hsqldb.JdbcDriver");
    dataSource.setConnectionUrl("jdbc:hsqldb:file:/tmp/itest.hdb");
    dataSource.setUsername("sa");
    dataSource.setPassword("");
    mbeanName = "jboss.jca:name=" + dataSource.getJndiName() + ",service=ManagedConnectionPool";
    assertMBeanDoesNotExist();
    createDataSource();
    deployItestEar(existingJBossServer, createItestEarOnTheFly(DataSourceStepsItestServlet.class));
    restartServer();
    assertMBeanExists();
    assertItestServlet(existingJBossHost, 8080, DataSourceStepsItestServlet.class);
    destroyDataSource();
    undeployItestEar();
    restartServer();
    assertMBeanDoesNotExist();
}

To be continued…

We’ve been using this approach to test our middleware configuration for quite some time now and it has provided us with a lot of confidence in our middleware integration code. There’s only one part I haven’t told you about yet; how to integrate this approach with your continuous integration using Maven and VMware. More on that next week…

Questions?

Get in touch with us to learn more about the subject and related solutions

Explore related posts