Embedded Web Services For Testing Web Applications from JUnit

Quite recently, I was working on a Java based web application. Everything went very well until I tried to test it. I was looking for a way to run my selenium based test cases with one click, because the manual deployment to tomcat was starting to be annoying. I didn’t manage to find any out of the box solutions so I had to do some extra work which I’ll present using a terribly simple example: one tiny JSP file:

<html>
  <head>
    <title>App</title>
  </head>
  <body>
    <span id="name">App</span>
  </body>
</html>

And the desired test case - IntegrationTest.java:

public class IntegrationTest {
  @BeforeClass
  public static void setUp() {
    tomcat.start();
    tomcat.deploy("spike");
    browser = new HtmlUnitDriver();
  }

  @Test
  public void test() {
    browser.get(tomcat.getApplicationUrl("spike"));
    assertEquals("App", browser.findElement(By.id("name")).getText());
  }

  @AfterClass
  public static void tearDown() {
    browser.close();
    tomcat.stop();
  }
}

It’s not necessary to start, stop and deploy the application for each test execution, so I’m doing it only once for the whole suite, hence the @BeforeClass and @AfterClass annotations.

Embedded Tomcat

It took me some time until I figured out how to use tomcat in an embedded way. In the first version, I tried to create a war file and deploy it to the base directory of tomcat, but that solution didn’t work, because tomcat has a very complicated way of handling war files and it was unable to load the jar files from the WEB-INF/lib folder. So that was a dead end, but the following version works just fine, although it is still not the best solution - EmbeddedTomcat.java:

public class EmbeddedTomcat {
  private Tomcat tomcat = new Tomcat();

  public void start() {
    try {
      // If I don't want to copy files around then the base directory must be '.'
      String baseDir = ".";
      tomcat.setPort(8090);
      tomcat.setBaseDir(baseDir);
      tomcat.getHost().setAppBase(baseDir);
      tomcat.getHost().setDeployOnStartup(true);
      tomcat.getHost().setAutoDeploy(true);
      tomcat.start();
    } catch (LifecycleException e) {
      throw new RuntimeException(e);
    }
  }

  public void stop() {
    try {
      tomcat.stop();
      tomcat.destroy();
      // Tomcat creates a work folder where the temporary files are stored
      FileUtils.deleteDirectory(new File("work"));
    } catch (LifecycleException e) {
      throw new RuntimeException(e);
    } catch (IOException e) {
      throw new RuntimeException(e);
    }
  }

  public void deploy(String appName) {
    tomcat.addWebapp(tomcat.getHost(), "/" + appName, "src/main/webapp");
  }

  public String getApplicationUrl(String appName) {
    return String.format("http://%s:%d/%s", tomcat.getHost().getName(),
        tomcat.getConnector().getLocalPort(), appName);
  }
}

As you can see, the current working directory can serve as a base directory, however tomcat creates a temporary folder for the compiled JSP files - work - which has to be removed after testing in order to keep the project clean. The src/main/webapp folder is also hardcoded into the test case, but it is very unlikely that I’ll move the source files around in the near future, so it is fine for the moment.

Let’s talk about versions a little bit. Tomcat 7 comes with an embedded version which is great. However, the latest version of tomcat - which was 7.0.29 at the time of writing this article -, didn’t work for me. I got a java.lang.OutOfMemoryError: Java heap space error, which I found very strange, because there was no way that running a web service for that tiny little JSP file required that much memory. I could have increased the available memory for eclipse, but that could only have been a short term solution, so I kept digging. It turned out that in 7.0.29 the way the ServletContainerInitilizers are scanning for the @HandleTypes annotations has changed, because they cache the result and when you have several classes on your classpath like in this case, you can easily run out of memory. The problem has already been fixed and will be delivered in 7.0.30 but until then I’m using version 7.0.28.

Embedded Jetty

I remembered that jetty has been offering an embedded solution for a while now, and the code isn’t that complicated - EmbeddedJetty.java:

public class EmbeddedJetty {
  private Server jetty;

  public void start(String appName) {
    try {
      jetty = new Server(8090);
      WebAppContext context = new WebAppContext();
      context.setContextPath("/" + appName);
      context.setWar("src/main/webapp");
      context.setServer(jetty);
      jetty.addHandler(context);
      jetty.start();
      jetty.setStopAtShutdown(true);
    } catch (Exception e) {
      throw new RuntimeException(e);
     }
  }

  public void stop() {
    try {
      jetty.stop();
    } catch (Exception e) {
      throw new RuntimeException(e);
    }
  }

  public String getApplicationUrl(String appName) {
    return String.format("http://%s:%d/%s",
            jetty.getConnectors()[0].getHost(),
            jetty.getConnectors()[0].getPort(), appName);
  }
}

This code compiled very nicely, but didn’t run. It turned out that the whole content of the jsp-2.1 folder had to be on the classpath in order to make it work. After that it worked like a charm. It was a bit slower than the tomcat version (~0.5s) but the difference wasn’t significant.

Although I had lots of trouble with tomcat and I wasted a lot of time with it, I’ll use it instead of jetty. Our production runs on tomcat and it is better to have the same environment in testing and production.

The implementation along with some examples are available on github. There is a launch.cfg folder for the example test cases: IntegrationTestWithTomcat.launch and IntegrationTestWithJetty.launch. The jetty version didn’t work when the tomcat-embed-jasper.jar was on its classpath so I had to have separate runtime classpaths for the two tests. If you want to run the test cases in eclipse, just go to the proper .launch file, right click and run as JUnit Test. Happy testing!


comments powered by Disqus