Skip to main content

Embedded Integration Testing of Web Applications

April 12, 2007

{cs.r.title}



Do you speak test? In that case: hello, web application:

public class WebIntegrationTest extends net.sourceforge.jwebunit.WebTestCase {

    public void testIndex() {
        beginAt("/index.html");
        assertTextPresent("Hello World!");
    }

    private org.mortbay.jetty.Server server;

    protected void setUp() throws Exception {
        // Port 0 means "assign arbitrarily port number"
        server = new org.mortbay.jetty.Server(0);
        server.addHandler(
                new org.mortbay.jetty.webapp.WebAppContext("src/main/webapp", "/my-context"));
        server.start();

        // getLocalPort returns the port that was actually assigned
        int actualPort = server.getConnectors()[0].getLocalPort();
        getTestContext().setBaseUrl("http://localhost:" + actualPort + "/my-context");
    }
}

This code runs with no application server and no separate deployment step, but it uses HTTP and a production web server. If this looks interesting, keep reading, and I'll show you how it works and a few more tricks, including how to use the same method to unit test a web application constructed with Spring-MVC and Hibernate.

The "Hello World" Embedded Integration Test

First: You can download the complete project to run the "hello world" application from the Resources section. To run the application, install Maven 2 and run mvn test. To use the code in an IDE, run mvn eclipse:eclipse or mvn idea:idea. (Or see instructions for Netbeans.) After you import the project into your workspace, the test should run in memory, just as a normal unit test.

If you want to follow the example by yourself, you can start out by installing Maven 2, and running the Maven Archetype command like so: mvn archetype:create -DgroupId=no.brodwall.insanejava.demo -DartifactId=web-demo -DarchetypeArtifactId=maven-archetype-webapp. This will create a skeletal Maven project.

Open the pom.xml file and add two more test dependencies: groupId jwebunit, artifactId jwebunit, version 1.2 and groupId org.mortbay.jetty, artifactId jetty, version 6.1.0. Create a directory named src/test/java and place the WebIntegrationTest shown above in it. For example, I would place mine in src/test/java/no/brodwall/web/integration/WebIntegrationTest.java.

If you have done everything right, your test will fail. This is because we haven't added an index.html file. You can simply rename the Maven-generated file src/main/webapp/index.jsp to index.html. Run the tests again and they should succeed. Congratulations, you have successfully run an embedded web integration test.

Spring Application Context

Now, let's try out a more sophisticated example: using Spring-MVC and interacting with the Controller to control the result. To do the rendering, I use FreeMarker, a "template engine," or in other words, "a generic tool to generate text output based on templates." FreeMarker combines textual templates with Java objects to create a view. Unlike JSPs, it does not require a separate compile step. This means that it starts up faster, which is good for unit tests. The use of FreeMarker requires two new dependencies in the Maven 2 pom (org.springframework:spring:2.0.1 and freemarker:freemarker:2.3.8--and remember to regenerate IDE files).

Okay, enough talk, here's another test for WebIntegrationTest:

public void testShowObject() {
    // Set up the objects that the controller depends on
    ModelObject pretendObject = new ModelObject();
    pretendObject.setAttributeOne("value one");
    pretendObject.setAttributeTwo(1234);

    // Set up the controller with the dummy ModelObject
    MyController controller = (MyController)getWebContextBean("controller");
    controller.setResult(pretendObject);

    // Execute the HTTP request
    beginAt("/my/controller.html");
    // Verify the result
    assertTextInElement("attributeOne", pretendObject.getAttributeOne());
    assertTextInElement("attributeTwo", Long.toString(pretendObject.getAttributeTwo()));    
}

ModelObject and MyController are classes in our application, so you should create them. MyController will be the controller in this Model-View-Controller application, which means that it will be the class that handles the actual request. The ModelObject will be a dependency it needs to do its job.

The test verifies that the values placed in the object that we injected into the controller are displayed on the resulting web page. Getting it to pass will require us to set up most of the infrastructure for a Spring-MVC application. The first, and maybe most interesting, question in your mind is probably: How do I get the Spring bean from the Web Context? Luckily, Spring provides a way to do this, but you need to get to the right objects. Here is the implementation. Take a deep breath:

import org.mortbay.jetty.webapp.WebAppContext;
import javax.servlet.ServletContext;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.WebApplicationContextUtils;

public class WebIntegrationTest extends WebtestCase {

    // ...

    private Object getWebContextBean(String beanName) {
        WebAppContext jettyWebAppContext = (WebAppContext) server.getHandler();
        ServletContext servletContext = jettyWebAppContext.getServletHandler().getServletContext();
        WebApplicationContext springWebAppContext =
            WebApplicationContextUtils.getRequiredWebApplicationContext(servletContext);
        return springWebAppContext.getBean(beanName);
    }

}

This test will currently fail. In order for Spring to have an application context, we need to set up a few more things: the src/main/webapp/web.xml must have a ContextLoaderListener, and we need the src/main/webapp/WEB-INF/applicationContext.xml. Personally, I prefer adding the ContextLoaderListener and contextConfigLocation first, and then getting the "FileNotFound" for the applicationContext.xml and adding that. This helps me remember what to do next. Here is the web.xml snippet:

<listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

src/main/webapp/WEB-INF/applicationContext.xml will in this case contain a single bean definition: <bean id="controller" class="no.brodwall.insanejava.webintegration.MyController" />).

For the actual integration, we need to map the /my path to Spring-MVC's DispatcherServlet and /my/controller.html to the controller. This is done in web.xml and the new my-servlet.xml. Add the org.springframework.web.servlet.DispatcherServlet to src/main/webapp/WEB-INF/web.xml:

<servlet>
  <servlet-name>my</servlet-name>
  <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
</servlet>

<servlet-mapping>
  <servlet-name>my</servlet-name>
  <url-pattern>/my/*</url-pattern>
</servlet-mapping>

Running the tests again will give the following useful (really!) message: "Could not open ServletContext resource [/WEB-INF/my-servlet.xml]". Create a new file, src/main/webapp/WEB-INF/my-servlet.xml:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
  <bean id="handlerMapping" class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
    <property name="mappings">
      <props>
        <prop key="/controller.html">controller</prop>
      </props>
    </property>
  </bean>
</beans>

After adding this code, running the test now gives me: "No adapter for handler [no.brodwall.web.MyController@80cac9]: Does your handler implement a supported interface like Controller?." Which of course it doesn't. So we make MyController implement org.springframework.web.servlet.mvc.Controller, which means implementing the handleRequest:

public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) {
    return new ModelAndView("showResult", "result", this.result);
}

Here, this.request was the value we set in the test.

Now I get a "404 not found." This is because I have not told Spring-MVC how to deal with the showResult view. I have to inform Spring-MVC about my decision to use FreeMarker. Here is the Spring configuration of FreeMarker in my-servlet.xml:

<bean id="viewResolver"
    class="org.springframework.web.servlet.view.freemarker.FreeMarkerViewResolver">
    <property name="cache" value="true" />
    <property name="prefix" value="" />
    <property name="suffix" value=".ftl" />
</bean>

<bean id="freemarkerConfig"
      class="org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer">
      <property name="templateLoaderPath" value="/views" />
</bean>

I can run the tests again. In that case, I get the sensible message: "Template showResult.ftl not found." Place this file in src/main/views/showResult.ftl:


<html>
<body>
  <p>Attribute One: <span id="attributeOne">${result.attributeOne}</span></p>
  <-- my only gripe with FreeMarker: By default, numbers are formatted fancy. string("#") overrides it -->
  <p>Attribute Two: <span id="attributeTwo">${result.attributeTwo?string("#")}</span></p>
</body>
</html>

The test now passes. Here it is again:

public void testShowObject() {
    // Set up the objects that the controller depends on
    ModelObject pretendObject = new ModelObject();
    pretendObject.setAttributeOne("value one");
    pretendObject.setAttributeTwo(1234);

    // Set up the controller with the dummy ModelObject
    MyController controller = (MyController)getWebContextBean("controller");
    controller.setResult(pretendObject);

    // Execute the HTTP request
    beginAt("/my/controller.html");
    // Verify the result
    assertTextInElement("attributeOne", pretendObject.getAttributeOne());
    assertTextInElement("attributeTwo", Long.toString(pretendObject.getAttributeTwo()));    
}

What Just Happened?

In the beginning of the article, we added the JWebUnit HTTP-based test library for web applications to the Maven project descriptor (POM). JWebUnit has allowed us to write simple unit tests that verify the output of the web pages using HTTP. It's very important to notice that despite being reasonably fast (about two seconds on my computer), these tests actually run with a real Java servlet container.

Figure 1 shows the composition of our Spring-MVC application stack.

Simple example with Spring-MVC
Figure 1. The composition of a Spring-MVC application stack

The tests do the following:

  1. Start the Jetty application server with a web context defined by our web.xml file.
  2. The web.xml file sets up a Spring-MVC controller and maps it to a URL.
  3. The test gets the controller out of the Spring Web Application Context, and set its result object.
  4. The test uses the JWebUnit framework to accesses the controller via HTTP.
  5. The test verifies that the HTML returned over HTTP matched our expectations; that is, the attributes displayed in the web page were those we injected into the controller.

This example is pretty basic, but it serves as a base for more advanced applications. Later, I will show how to put together a complete CRUD (Create Retrieve Update Delete) application with Hibernate, but first, I want so show how you can test this application interactively.

Starting Jetty for Interactive Use

For minimal hassle, I usually create a class with a main method. This is src/test/java/no/brodwall/web/WebServer.java:

public class WebServer {
    public static void main(String[] args) throws Exception {
        Server server = new Server(8080);
        server.addHandler(new WebAppContext("src/main/webapp", "/web-demo"));
        server.start();
    }
}

Run the main class, and go to http://localhost:8080/web-demo/my/controller.html. You will get a nice stack trace from FreeMarker. This is because the result object returned by the controller is null. Create a default object in src/main/webapp/WEB-INF/applicationContext.xml to fix this problem:

<bean id="controller" class="no.brodwall.web.MyController">
  <property name="result">
    <bean class="no.brodwall.web.ModelObject">
      <property name="attributeOne" value="one" />
      <property name="attributeTwo" value="2" />
    </bean>
  </property>
</bean>

You have to restart the application server before you can behold the beauty, but when you do, you should be able to feast your eyes on a very simple web page. Like JSPs, FreeMarker templates will be reloaded at runtime if you edit them. Try and modify src/main/webapp/views/showResult.ftl and hit the Refresh button in your browser.

Alternatively, you can use the Maven 2 Jetty plugin, by adding the <plugin> section to the pom.xml file. I prefer having my own main method; that way, debugging the application is much simpler.

Integration Testing a Simple CRUD Application

The introduction showed how to get started testing and implementing a web application. I will now describe how to test a full-fledged application. The application will allow for retrieving and updating of a domain object, demonstrating the R and U of the acronym CRUD (Create Retrieve Update Delete). The C (create) and the D (delete) are left as exercises to the reader.

The article will not describe how to implement the code to make the tests pass, but only the tests themselves. If you're curious about the specifics, please see the companion source code (in the Resources section).

Index: Show a List of Objects

The full application will be a CRUD application for a single domain class. I am using a class named Category, as seen in Figure 2, as an example. Each category has a name and a year it was created. Categories also have subcategories, but we will ignore those for now. I will start the testing effort using a test doubles for the Data Access Object (DAO). This lets me focus on the web code first. In order to make the tests pass, you should implement your FakeCategoryDAO as simply as you can while getting the tests to run.

Category domain model
Figure 2. The Category class

Here is a test showing how our web application shows a list of categories. Put this test in a new CategoryWebIntegrationTest class with the same setup of Jetty as our simple test:

public void testIndex() {
    // Create a Fake DAO and test data

    CategoryDAO categoryDao = new FakeCategoryDAO();
    Category firstCategory = new Category("first category");
    Category secondCategory = new Category("second category");
    categoryDao.insert(firstCategory);
    categoryDao.insert(secondCategory);

    // Inject the DAO it into the controller
    CategoryListController controller = (CategoryListController)getWebContextBean("listController");
    controller.setDao(categoryDao);

    // Go the the index page
    beginAt("category/index.html");
    // Verify that all data we expect is present
    assertTablePresent("categories");
    assertTextInTable("categories", firstCategory.getName());
    assertTextInTable("categories", secondCategory.getName());
}

Like my simpler test, I manipulate the model objects behind the controller, in this case the DAO that contains all our categories. The test consists of four steps:

  1. Set up test data: Create the underlying data to be displayed and manipulated.
  2. Set up test objects: Replace the production DAO with the test double. We have to do this for all relevant controllers.
  3. Execute the code to be tested: In our case, this means executing a dialog towards a web page using JWebUnit.
  4. Check the result: In our case, this means checking the values displayed on a particular web page.

All tests of the CRUD web application follow the same pattern. Again, see the companion source code on how to make the tests pass.

testShowDetail

Let's zoom in on one of these categories:

public void testDetail() {
    // Create a Fake DAO and test data
    CategoryDAO categoryDao= new FakeCategoryDAO();
    Category category = new Category("first category", 2007);
    categoryDao.insert(category);

    // Inject the DAO it into the controllers
    CategoryListController controller = (CategoryListController)getWebContextBean("listController");
    controller.setDao(categoryDao);
    CategoryController categoryController = (CategoryController)getWebContextBean("categoryController");
    categoryController.setDao(categoryDao);

    // Go to the list page and navigate to our object
    beginAt("category/index.html");
    clickLinkWithText(category.getName());

    // Verify that all attributes are displayed
    assertTextInElement("name", category.getName());
    assertTextInElement("creationYear", Integer.toString(category.getCreationYear()));
}

I added a creationYear property to the Category class, and a constructor that sets it up. I also created a new controller: CategoryController. Just creating dummies for these will make the code compile. Add categoryController to src/main/webapp/WEB-INF/applicationContext.xml to make it available in the application context.

The interesting parts of the test are in the second half. We expect the index page to have a link to our Category object. And we expect the target of the link to display all properties of the Category.

testEditName

If you follow along and try to write the code to make the tests pass as well, you will have noticed that progress is probably slow at this point. We are still creating the infrastructure, so this is to be expected. Personally, I notice a markable speed-up at this point. Let's try to edit the name of a category:

public void testEditName() {
    // Refactor out the setup of the test data from previous tests
    populateTestdata();

    String newName = "new subcategroy 1 name";

    // Go to and verify the edit page
    beginAt("category/edit.html?id=" + category.getId());
    assertFormElementEquals("name", category.getName());
    // Update and submit the form
    setFormElement("name", newName);
    submit();

    // Check the update
    assertTextInElement("name", newName);
    // To avoid a temporary value from being displayed

    beginAt("category/show.html?id=" + category.getId());
    assertTextInElement("name", newName);
}

Notice that I have refactored the setup of the test data into a new method. I have updated the other tests correspondingly as well.

We're Not Out of the Woods Yet

I have shown how you can test drive a simple CRUD web application using JWebUnit and Jetty. But Spring-MVC and the realities of Java web applications still have a few surprises in stock for us. Taking the code all the way to production requires us to persist the categories. I will implement a simple Hibernate DAO for this.

The CategoryDAOTest

Our first order of business is to implement the CategoryDAO interface using Hibernate. This interface has been evolving as we have implemented new functional requirements. Parallel to these changes, you should develop a test class for the CategoryDAO. The new requirement is to make this test case work with the HibernateCategoryDAO and to wire the application up to use the HibernateCategoryDAO. This requires a new trick with Jetty: getting the DataSource injected correctly.

Here is the current CategoryDAO interface we need to implement our functional requirements. While creating the basic web tests, I used a "fake" implementation of this that stores all the objects in memory. This makes me able to create the tests without slowing down to mess around with Hibernate.

public interface CategoryDAO {

    void insert(Category category);

    Collection<Category> listAll();

    Category get(long id);

}

I strongly recommend that you develop in-process integration tests for the HibernateCategoryDAO. For more information on this, see my java.net article on the subject, " Unit Testing Hibernate Mapping Configurations." An example of a test method in CategoryDAOTest would be testing that objects are persisted correctly:

public void testFindAll() {
    Category fooCategory = new Category("foo");
    Category barCategory = new Category("bar");
    Category bazCategory = new Category("baz");
    categoryDAO.insert(fooCategory); categoryDAO.insert(barCategory); categoryDAO.insert(bazCategory);
    categoryDAO.flushAndEvict();

    Collection<Category> expected = Arrays.asList(fooCategory, barCategory, bazCategory);
    Collection<Category> actual = categoryDAO.listAll();
    // Use sets to make sure that ordering is ignored
    assertEquals(new HashSet<Category>(expected), new HashSet<Category>(actual));
}

Or testing that unsaved changes are not returned:

public void testUnsavedChanges() {
    Category category = new Category("foo");
    categoryDAO.insert(category);

    Category updatedCategory = categoryDAO.get(category.getId());
    updatedCategory.setName("bar");

    Category storedCategory = categoryDAO.get(updatedCategory.getId());
    assertEquals("foo", storedCategory.getName());
}

This last test is interesting because it will fail! This may lead to hard-to-find bugs in our code. My Hibernate testing article (cited above) explains how to deal with this in detail, so I will just explain it briefly here.

The problem is that the DAO returns the same object instance. This is because our fake DAO just stores objects in memory, but we will have the same problem with Hibernate. Like most object-relation mapping frameworks, Hibernate uses the concept of a session cache (also known as a first-level cache). The session cache is made to ensure that we don't have multiple instances of what is meant to be the same object lying around. To avoid hitting this problem in our test, we can introduce the concept of evicting objects from the cache. Here is the updated test:

public void testUnsavedChanges() {
    Category category = new Category("foo");
    categoryDAO.insert(category);
    categoryDAO.flushAndEvict();

    Category updatedCategory = categoryDAO.get(category.getId());
    categoryDAO.flushAndEvict();
    updatedCategory.setName("bar");

    Category storedCategory = categoryDAO.get(updatedCategory.getId());
    assertEquals("foo", storedCategory.getName());
}

We will have to implement flushAndEvict for the FakeCategoryDAO as well. We can do this in several ways, but the fastest is to just clone all the objects. See the companion source code for details.

With a decent CategoryDAOTest in place, we can now create the Hibernate version of the test and of the DAO. I recommend using Spring's Hibernate support, specifically AnnotationSessionFactoryBean and HibernateTemplate, for this. See the companion source code if you're interested in the details.

HibernateCategoryDAOTest is a subclass of CategoryDAOTest that sets the CategoryDAO member variable of the superclass to HibernateCategoryDAO. I have implemented this by letting CategoryDAOTest initialize categoryDAO with a call to the overrideable method createCategoryDao:

public class HibernateCategoryDAOTest extends CategoryDAOTest {

    protected CategoryDAO createCategoryDao() {
        return new HibernateCategoryDAO();
    }

}

Running HibernateCategoryDAOTest will show fail until we initialize the HibernateCategoryDAO correctly with the HibernateTemplate. For now, let's do this in createCategoryDao:

protected CategoryDAO createCategoryDao() {
    SingleConnectionDataSource dataSource = new SingleConnectionDataSource();
    dataSource.setDriverClassName("org.hsqldb.jdbcDriver");
    dataSource.setUrl("jdbc:hsqldb:mem:test");
    dataSource.setUsername("sa");
    dataSource.setSuppressClose(true);

    Properties hibernateProperties = new Properties();
    hibernateProperties.setProperty(Environment.HBM2DDL_AUTO, "create-drop");

    AnnotationSessionFactoryBean sessionFactoryBean = new AnnotationSessionFactoryBean();
    sessionFactoryBean.setDataSource(dataSource);
    sessionFactoryBean.setHibernateProperties(hibernateProperties);
    sessionFactoryBean.setAnnotatedClasses(new Class[] { Category.class });

    try {
        sessionFactoryBean.afterPropertiesSet();
    } catch (Exception e) {
        throw new RuntimeException("SessionFactory misconfiguration", e);
    }
    HibernateTemplate hibernateTemplate = new HibernateTemplate((SessionFactory)sessionFactoryBean.getObject());

    return new HibernateCategoryDAO(hibernateTemplate);
}

This code is long and annoying. The most important things to notice are the facts that we're using Spring's SingleConnectionDataSource as a test data source, and specifying the HSqlDb in-memory database as the database. This means that all it keeps the data in memory. As a result, any changes are lost when the VM shuts down. This means we don't need to clean up anything. But we will have to tell Hibernate to "create-drop" the tables in the database, so Hibernate generates them automatically when our application runs.

There is a common problem with tests that use the database: one of our test methods left garbage in the database that made the other tests fail. There are a few different ways of fixing it, but here, the best is to use the Transaction Rollback Teardown pattern:

private PlatformTransactionManager transactionManager;
private TransactionStatus status;

protected void setUp() throws Exception {
    super.setUp();

    transactionManager = new HibernateTransactionManager(sessionFactory);
    TransactionDefinition txDef = new DefaultTransactionDefinition();
    status = transactionManager.getTransaction(txDef);
}

protected void tearDown() throws Exception {
    transactionManager.rollback(status);
    super.tearDown();
}

Now we have a working Hibernate implementation of the CategoryDAO and a CategoryIntegrationTest that shows that we can display an object from a CategoryDAO on a web page. What happens if we put them together?

HibernateCategoryIntegrationTest

Now all the pieces work; we just have to put them together. We create a subclass of the CategoryIntegrationTest for this. We have to make sure to change CategoryWebIntegrationTest to support creating something other than a FakeCategoryDAO. Do this by changing the populateTestdata line CategoryDAO categoryDao = new FakeCategoryDAO(); to CategoryDAO categoryDao = createCategoryDAO();. Make createCategoryDAO() protected so HibernateCategoryWebIntegrationTest can override it:

public class HibernateCategoryWebIntegrationTest extends CategoryIntegrationWebTest {

    protected CategoryDAO createCategoryDao() {
        // Copied from HibernateCategoryDAOTest
    }

}

Using Real(er) DataSources

Before we continue, it's a good idea to do some refactoring. The creation of our HibernateSessionFactory is pretty complex. Let's put it in a separate class, named CategorySessionFactoryBean. In both HibernateCategoryWebIntegrationTest and HibernateCategoryDAOTest I'd like createCategoryDao to look like this:

protected CategoryDAO createCategoryDAO() {
    DataSource dataSource = CategorySessionFactoryBean.inmemoryDataSource();
    SessionFactory sessionFactory = CategorySessionFactoryBean.create(dataSource);
    return new HibernateCategoryDAO(new HibernateTemplate(sessionFactory));
}

Put CategorySessionFactoryBean in the main code, so the servlet code can use it as well.

Second, I restructure the web integration test classes. Make CategoryWebIntegrationTest abstract and create a new FakeDAOCategoryIntegrationTest subclass that overrides CategoryWebIntegrationTest.createCategoryDAO(). This means that we only inherit from abstract classes. In my experience, this leads to better structure. In this particular case, I want to do this before I change the Spring configuration to return a HibernateCategoryDAO.

Now we're ready to update the test WebService main class we made to test the application interactively. We want it to use Hibernate as well. In order to do that, let's make sure that the normal application context uses Hibernate. First, we update HibernateCategoryWebIntegrationTest.createCategoryDAO to use the DAO in the Spring application context:

protected CategoryDAO createCategoryDAO() {
    return (CategoryDAO)getWebContextBean("categoryDao");
}

In order to get this to work, you will have to update how the data source is configured in src/main/webapp/WEB-INF/applicationContext.xml:

<bean id="dataSource" class="org.springframework.jndi.JndiObjectFactoryBean">
  <property name="jndiName" value="jdbc/primaryDs" />
  <property name="resourceRef" value="true" />
</bean>

We declare the data source as a JndiObjectFactoryBean. This means that we need to set up the JNDI name jdbc/primaryDs before we start up Jetty.

Jetty and JNDI

We want to get the data source from JNDI. This means that we have to configure Jetty to give the web application a JNDI data source. It was surprisingly hard to find the documentation for this, but as it turns out, it is very simple. We have to create a new org.mortbay.jetty.plus.naming.Resource object. The constructor for this object automatically registers itself with JNDI. In order to get the JNDI functionality to work, we must add dependencies to jetty-plus and jetty-naming in our project's Maven 2 pom.xml file. All our Jetty dependencies should use the same version. (Jetty developers, if you read this: please make the dependencies from jetty-plus to jetty and jetty-naming compile dependencies, not provided dependencies, so I wouldn't have to do this!)

When we've added the new dependencies, we can change CategoryWebIntegrationTest.setUp to create the Resource for the data source before Jetty is started.

protected void setUp() throws Exception {
    server = new org.mortbay.jetty.Server(0);
    WebAppContext webAppContext = new WebAppContext("src/main/webapp", contextName);
    webAppContext.setConfigurationClasses(new String[] {
        // These are default, but will be overwritten if we don't specify them
        "org.mortbay.jetty.webapp.WebInfConfiguration",
        "org.mortbay.jetty.plus.webapp.Configuration",
        "org.mortbay.jetty.webapp.JettyWebXmlConfiguration",
        "org.mortbay.jetty.webapp.TagLibConfiguration",
        // This enables the <resource-ref> section of web.xml
        "org.mortbay.jetty.plus.webapp.EnvConfiguration",
    });
    // This is needed to avoid error on subsequent Environment registrations
    org.mortbay.jetty.plus.naming.NamingEntry.setScope(NamingEntry.SCOPE_GLOBAL);
    // This actually registers the resource
    new org.mortbay.jetty.plus.naming.Resource(
                 "jdbc/primaryDs",
                 CategorySessionFactoryBean.inmemoryDataSource());
    server.start();

    int actualPort = server.getConnectors()[0].getLocalPort();
    getTestContext().setBaseUrl("http://localhost:" + actualPort + "/my-context");
}

Finally, we need to update src/main/webapp/WEB-INF/web.xml and add the <resource-ref> section that lets the container know we expect a DataSource:

<resource-ref>
  <description>Primary Data Source</description>
  <res-ref-name>jdbc/primaryDs</res-ref-name>
  <res-type>javax.sql.DataSource</res-type>
  <res-auth>application</res-auth>
</resource-ref>

This completes the wiring together of the application.

To the Server!

Of course, in production, we probably want to use a "real" database and a production server. This means exchanging the jdbc/primaryDs resource with a connection to MySQL, Oracle, or whatever database you use. I have created MySqlCategoryWebIntegrationTest and MySqlCategoryDAOTest, which uses MySQL to test. See the companion source code for details. I also modified the WebServer class to use MySQL.

The trickiest difference between test and production scenarios is how to set Hibernate's HBM2DDL_AUTO property. I have implemented this in CategorySessionFactoryBean in the companion source code.

What about the application server?

Personally, I would strongly recommend deploying on Jetty in production. Jetty is a production-ready server that runs critical business. Despite the fact that it receives much less attention than Tomcat and JBoss, Jetty runs on about a third as many publicly available sites as Tomcat (Apache-Coyote in the Netcraft study).

There are two ways in which Jetty is commonly used: either you can use Jetty as a standalone container, like Tomcat or any other, or you could build your own Main-Class that starts up Jetty.

To deploy on a standalone Jetty instance, we can download Jetty and unpack it. Jetty lets us drop-deploy WAR files, but we have to set up the DataSource in a jetty-env.xml file. This should be located in src/main/webapp/WEB-INF/jetty-env.xml in our project. See the companion source code for an example of this file. See Jetty's documentation for details on jetty-env.xml. The Jetty documentation also explains how to add DataSources for various databases.

Use the command mvn package to build the WAR file into the target directory of your project. Copy this file to JETTY_HOME/webapps-plus. Start Jetty by running java -jar start.jar etc/jetty.xml etc/jetty-plus.xml from JETTY_HOME. The web application should now be available.

The simplest way of deploying on Tomcat is to add a "context" file in Tomcat's scanning directory ($CATALINA_HOME/conf/[engine_name]/[host_name]). If you use the location of your WAR file in the Maven repository as the docBase for Tomcat, Tomcat will pick up any changes when you execute mvn install. See the example context file src/main/tomcat/web-demo.xml in the companion source code for an example.

Conclusion, and Unfinished Business

In this article, I have showed how Jetty can help us bring testing all the way from early development into testing your real production environment. Running Jetty in-process in an embedded integration test lets us control the data and context of our test and lets us test logic related to views, as well as problems that only show up when we put our application in a real container. Figure 3 shows how Jetty runs inside the same VM as the tests and is controlled from the unit test code.

Embedded testing with Jetty
Figure 3. How the tests set up the elements of the application

This article has also given an introduction to building web applications with Spring and Hibernate. However, there are aspects of Spring-MVC and Hibernate that we should have covered. Specifically, when we display more complex object graphs, we will have to deal with having lazily loaded objects resolved while we're rendering the view. This is handled through Spring's OpenSessionInViewFilter. Introducing OpenSessionInViewFilter will also require us to wise up about transactions on the DAO. These problems are left as an exercise for the reader. Leave me a comment if you would like a hint.

I hope this article gave you some insight into how to effectively test web application. Please, leave a comment if something went wrong along the way, if you have discovered a better way of doing the same thing, or if you just enjoyed the article.

Resources

Johannes Brodwall is currently lead Java architect at BBS, the company that operates Norway's banking infrastructure
Related Topics >> Testing   |