Beliebte Suchanfragen

Cloud Native

DevOps

IT-Security

Agile Methoden

Java

//

Writing lightweight REST integration tests with the Jersey Test Framework

3.5.2012 | 6 minutes of reading time

Writing REST services with JAX-RS (and its reference implementation Jersey) is easy. A class annotated with @Path and some methods with @GET, @POST, … annotations is enough for a fully functional REST service. Real world applications however are more complex. There are request-filters for authorization and access control, context providers for injecting data-access-objects, mappers that convert exceptions to appropriate HTTP responses, MessageBodyReaders and -Writers to convert JSON and XML to and from Java objects, and so on.

All these components can (and should) be tested using unit tests. But this is not enough. To be sure, that these components work together correctly, integration tests are needed. These can be costly to run. They always need the full environment to be configured and running. And the more complex an application, the more complex it is to set up this environment (webserver, database, search engine, message queue, …).

The Jersey Test Framework fills the gap between simple unit tests and full-fledged integration tests. It offers the possibility to write lightweight integration-tests, that do not need any external resources to be available. The web-container, where all components (resources, filters, mappers, …) run, is configured and started on-the-fly. A short introduction to the Jersey Test Framework can be found in the jersey documentation.

Example REST Service and Client

The following explanations are based on a simple example-application. The complete code is available on github . To keep things short, I only included the most interesting parts. And because it’s always interesting, if something goes wrong, I picked a resource, where exceptions are thrown.

1@Path("/todo")
2public class TodoResource {
3    @Context
4    private TodoService todoService;
5 
6    // ...
7 
8    @DELETE
9    @Path("/{todo}")
10    public void removeTodo(@PathParam("todo") String todoToRemove) {
11        // throws a TodoNotFoundException, if the todo can not be found
12        todoService.removeTodo(todoToRemove);
13    }
14}

The TodoService is used to persist todo-items. In the above code, it should remove one item (from the database). If the item does not exist, the method removeTodo throws a TodoNotFoundException. This exception is transformed into a HTTP 404 response by the following exception mapper:

1@Provider
2public class NotFoundMapper implements ExceptionMapper {
3    @Override
4    public Response toResponse(TodoNotFoundException e) {
5        return Response.status(Response.Status.NOT_FOUND)
6                   .entity("todo-not-found").build();
7    }
8}

The mapper not only creates a 404-response, it also packs details about the exception into the response-body (a simple string in this case). This information can be used by clients to find out, what exactly went wrong. In our case, the client throws a ClientSideTodoNotFoundException when he encounters a 404 response with body “todo-not-found”. It could simply throw the same TodoNotFoundException, but in order to be able to distinguish exceptions thrown on client- and server-side, we use a different exception.

1public class TodoClient {
2 
3    private final String uri;
4 
5    public TodoClient(String uri) {
6        this.uri = uri;
7    }
8 
9    public WebResource resource(String todo) {
10        return client.resource(uri).path("/todo/"+todo);
11    }
12 
13    public void removeTodo(String todoToRemove) {
14        try {
15            resource(todoToRemove).delete();
16        } catch (UniformInterfaceException e) {
17            int status = e.getResponse().getClientResponseStatus();
18            String body = e.getEntity(String.class);
19            if (status == Response.Status.NOT_FOUND) &&
20                    "todo-not-found".equals(body)) {
21                throw ClientSideTodoNotFoundException();
22            } else {
23                throw e;
24            }
25        }
26    }
27}

Integrated Client-Server-Tests

The following test will check, that any TodoNotFoundException thrown by the TodoService is correctly converted in a HTTP-Response, which the client converts to the appropriate ClientSideTodoNotFoundException. Thus we can test the whole stack (except the database-layer), while still being able to run the tests without external infrastructure (because we mock the database-layer).

1class TodoResourceTest extends JerseyTest {
2 
3    public static TodoService todoServiceMock = Mockito.mock(TodoService.class);
4 
5    @Override
6    public WebAppDescriptor configure() {
7        return new WebAppDescriptor.Builder()
8                .initParam(WebComponent.RESOURCE_CONFIG_CLASS,
9                      ClassNamesResourceConfig.class.getName())
10                .initParam(
11                      ClassNamesResourceConfig.PROPERTY_CLASSNAMES,
12                      TodoResource.class.getName() + ";"
13                              + MockTodoServiceProvider.class.getName() + ";"
14                              + NotFoundMapper.class.getName()).build();
15    }
16 
17    @Override
18    public TestContainerFactory getTestContainerFactory() {
19        return new GrizzlyWebTestContainerFactory();
20    }
21 
22    @Test(expected = ClientSideTodoNotFoundException.class);
23    public void removeTodoShouldThrowNotFoundException() {
24        final String todo = "test-todo";
25        final TodoClient todoClient = new TodoClient(getBaseURL());
26        Mockito.when(todoServiceMock.removeTodo(todo))
27            .thenThrow(new TodoNotFoundException());
28        todoClient().removeTodo(todo);
29    }
30 
31    @Provider
32    public static class MockTodoServiceProvider extends
33           SingletonTypeInjectableProvider {
34        public MockTodoServiceProvider() {
35            super(TodoService.class, todoServiceMock);
36        }
37    }
38}

Some explanations:
Because we do not want to connect to an external database, the TodoService has to be mocked. This is done by defining a provider, that injects a mocked TodoService. Because we also want to configure the mock-object inside our test, the MockTodoServiceProvider is defined as inner class and the mock-object is stored in a class variable of our test class.

The test is configured to use a GrizzlyWebTestContainer. See the last part of this blog-post for advantages and disadvantages of using other containers. In the configure() method, we tell jersey, where to find the classes for resources and providers.

In the test method itself, the TodoService mock is instructed to throw a TodoNotFoundException, when the removeTodo() method is called. If everything works correct, then the client will throw the expected ClientSideTodoNotFoundException and the test passes.

Tips and Tricks

What follows is a list of tips, that I hope will be useful for those, who start working with the Jersey Test Framework.

Decide what type of container to use before writing tests
There are two kinds of containers available for the jersey test framework: high-level servlet containers and low-level containers. Both have advantages and disadvantages.

The high-level servlet containers offer the full functionality of a servlet container, automatically injecting instances of HttpServletRequest, … . If your application relies heavily on servlet specific classes, these containers will be your first (and probably only) choice. The servlet functionality comes at a price: All implementations need to open system ports, which makes the tests more fragile and also a little bit slower. Another drawback of using real servlet containers is, that you don’t have direct access to the instances of your resource- and provider-classes. To allow the use of mock-objects, you must work around this problem, for example by assigning context objects to static fields, as we did with the mocked TodoService.

Low-level containers on the other hand, allow you to directly modify the ResourceConfig used. You have direct access to all instances of resource-, provider- and filter-classes used for the rest service. This simplifies mocking. So if you don’t rely on the servlet-api, you’ll probably go for a low-level container.

Do not use WebAppDescriptor for low-level containers
Althoug possible, I do not recommend using WebAppDescriptors for low-level containers. The reason lies in the method LowLevelAppDescriptor.transform(), which is used to transform a WebAppDescriptor to a LowLevelAppDescriptor. The method simply ignores all non-boolean init-params. Moreover, there is a bug when using the property com.sun.jersey.config.property.packages with multiple (colon-separated) package-names. Even if these shortcomings get fixed, you should not rely on the transform() method. The power of low-level containers lies in the possibility to directly modify the used ResourceConfig, which is only possible when using a LowLevelAppDescriptor.

Speedup jersey tests
Because the JerseyTest base class starts a new web-container before each test, the tests are rather slow. One possibility to speed them up, is to start the web-container only once per test-suite. A implementation for a base class doing this is included in the example-application on github .

InMemoryTestContainer with filters
The InMemoryTestContainer is the only container, that does not open any real ports on the system. Of course, being a low-level container, no servlet-specific functionality is available with this container. But if you do not rely on the servlet-api too much, this container is the perfect choice to write really fast and lightweight integration tests.

However, the InMemoryTestContainer has another drawback: you cannot declare any request- or response-filters, because they are overridden by logging filters. To work around this problem, I implemented my own in-memory-test-container (basically only copying the original code and removing the logging filters). The code is also included in the example application .

share post

Likes

0

//

More articles in this subject area

Discover exciting further topics and let the codecentric world inspire you.

//

Gemeinsam bessere Projekte umsetzen.

Wir helfen deinem Unternehmen.

Du stehst vor einer großen IT-Herausforderung? Wir sorgen für eine maßgeschneiderte Unterstützung. Informiere dich jetzt.

Hilf uns, noch besser zu werden.

Wir sind immer auf der Suche nach neuen Talenten. Auch für dich ist die passende Stelle dabei.