Web App Testing With Jetty

Jetty is a great way of testing your web-apps that are destined for a simple web server as opposed to a J2EE server. If you’re deploying to an app server like JBOSS then you should check out Arquillian but if you’re deploying to Tomcat or Jetty then you should look into Jetty for your testing.

Assumptions

The only assumption here is that your web application is container agnostic. By this I mean that the application doesn’t make any assumptions about the container it’s running in and instead relies solely on the servlet interfaces. This is a good idea in general irrespective of testing.

The other assumption is that your application is modeled as a maven WAR module or at least has a maven WAR module in it as a multi-part maven module. The fixture shown below assumes that you’re running the test from the WAR module.

Basic Idea

The use of Jetty allows you to test your webservices exactly as they would be deployed in the wild. This allows you to verify that any additional behavior related to deployment such a spring-security or transport level encodings will also be included in the test. To be clear, this is suitable for integration level tests as opposed to unit tests. If you’re testing simple isolated functions then you’re better off testing with an offline JUnit which will execute with very little setup or ceremony. The small amount of code and configuration shown here will likely be an order of magnitude slower than unit tests so proceed with that in mind. I routinely use this for service level integration tests where I invoke a service and then assert that the system is side-effected in some observable way (usually through another service call).

Dependencies

You need to add a couple of dependencies to your web module in order for this to work. The most recent version of a stable Jetty at the time of this writing is 9.3.10.v20160621. Your mileage may vary with the 9.4.x versions.

<dependency>
    <groupId>org.eclipse.jetty</groupId>
    <artifactId>jetty-server</artifactId>
    <version>${jetty.version}</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.eclipse.jetty</groupId>
    <artifactId>jetty-webapp</artifactId>
    <version>${jetty.version}</version>
    <scope>test</scope>
</dependency>        

Fixture for Launching Your App

The following fixture will launch an instance of Jetty that’s setup to serve your web-app from the source files in your module. The fixture accepts some basic params for launching including:

  • port: The port to listen on
  • webxml: reference to the web.xml File
  • webappDir: reference to the webapp directory
  • contextpath: context path that the web app will be accessible on
public class JettyWebAppRunner {
    private final Server server;
    private final WebAppContext context;

    public JettyWebAppRunner(int port, File webxml, File webappDir, String contextPath) 
    throws Exception {
        assert webxml.isFile();
        assert webappDir.isDirectory();
        if (!contextPath.startsWith("/")) {
            contextPath = "/" + contextPath;
        }
        server = new Server(port);
        context = new WebAppContext();
        context.setDescriptor(webxml.getAbsolutePath());
        context.setResourceBase(webappDir.getAbsolutePath());
        context.setContextPath(contextPath);
        context.setParentLoaderPriority(true);
        context.setThrowUnavailableOnStartupException(true);

        server.setHandler(context);
    }

    public void start() throws Exception {
        server.start();
    }

    public void stop() throws Exception {
        server.stop();
        server.destroy();
    }
}

Your test suite should construct and start the JettyWebAppRunner in its @Before or @BeforeClass and then run its tests while being sure to stop it in its @After or @AfterClass. Keep in mind that if you’re running multiple tests in the JUnit then you’ll be starting and stopping the web app multiple times which may not be what you want to do.

Service Being Tested

The service in this case is an overly simplified example to demonstrate how to invoke services with this framework. It contains a single @GET and a simple POJO that’s converted to JSON when invoked.

public class HelloWorldServiceImpl implements HelloWorldService {
    public Greeting hello() {
        return new Greeting().setMessage("Hello from Spring JAX-RS service");
    }
}

POJO Payload

Greeting is also a simple POJO. It’s converted to JSON via the Jackson library, the configuration of which is not shown here.

@XmlAccessorType(XmlAccessType.FIELD)
public class Greeting {
    private String message;

    @Generated("generated by IDE")
    public String getMessage() {
        return message;
    }

    @Generated("generated by IDE")
    public Greeting setMessage(String message) {
        this.message = message;
        return this;
    }
}

Test Class Example

Here’s a toy example of a test against a web service:

public class WebAppTest {

    private JettyWebAppRunner runner;

    @Before
    public void init() throws Exception {
        runner = new JettyWebAppRunner(8080, 
                new File("src/main/webapp/WEB-INF/web.xml"), 
                new File("src/main/webapp"), 
                "/");
        runner.start();
    }

    @After
    public void tearDown() throws Exception {
        runner.stop();
    }

    @Test
    public void test() throws Exception {
        Client client = ClientBuilder.newClient();
        try {
            Response response = client.target("http://localhost:8080/services/api/hello")
                    .request()
                    .accept(MediaType.APPLICATION_JSON)
                    .get();
            assertEquals(200, response.getStatus());
            String json = response.readEntity(String.class);
            assertEquals("{\"message\":\"Hello from Spring JAX-RS service\"}", json);
        } finally {
            client.close();
        }
    }
}

Summary

This approach has a number of advantages but it’s worth reiterating that the main disadvantage is the time it takes to run these tests. Your typical unit test might take 100ms to run while even the simplest web app is going to take 2 seconds or more to run and possibly much longer if you have a monolithic web app. You should not use this approach in place of unit tests! This is designed for integration tests.

Service Deployment

If your test is using the basic web client and URI approach as shown above, then it’s an opportunity to ensure that the service is deployed to the endpoint that you expected. If your service makes use of @*Param or other annotations then you can ensure that any behavior related to those (include @DefaultValue) is configured as expected. It’s not so much that you’re testing the JAX-RS framework as you’re testing that you’ve applied the annotations correctly.

Transport Level Testing

By testing the service call with a real GET or POST or similar, you’re including a test for the configuration of the object marshalling from the service. This is meaningful in cases where you’re relying on a library like Jackson or similar to handle the marshaling based on annotations which are easy to omit or mis-configure since there aren’t compile time checks.

At the transport level you’re also entering the app through its primary web interface and thus will interact with spring security or whatever other security mechanism you having in placce.

Easy Debugging

I’ve found it very easy to recreate bugs through various service calls in an integration test and set a breakpoint in the service to step through the relevant part of the call. Consider that all of this is running in the same JVM so setting debugging is really easy by simply setting a breakpoint in the code and running the test in debug mode in your favorite IDE. Attaching to a remote container is not especially difficult but it’s a small cost that adds up over time.

Better Code Coverage

You can achieve better code coverage with this approach. Again, since it’s all running within the same JVM, code coverage tools like Coverity and Cobretura have an easy time of recording the service method invocations from your test code. This would be more difficult to aggregate the results if you were running against a remote server.

Written on July 6, 2016