tag:blogger.com,1999:blog-69783685263551605072018-03-07T16:15:36.986+01:00MJL OnlineProgramming, Linux and stuff...Michael Lexhttps://plus.google.com/116211833963066507726noreply@blogger.comBlogger2125tag:blogger.com,1999:blog-6978368526355160507.post-24458208137539398232012-04-10T10:07:00.001+02:002012-04-10T10:07:36.108+02:00Writing lightweight REST integration tests with the Jersey Test Framework<p>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 (web-server, database, search-engine, message-queue, ...). </p> <p>The Jersey Test Framework 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. Moreover, it is possible to provide mocks for data-access-objects and thus extinguish the need for external services. </p> <p>A short introduction to the Jersey Test Framework can be found in the jersey documentation: <a href="http://jersey.java.net/nonav/documentation/latest/test-framework.html">http://jersey.java.net/nonav/documentation/latest/test-framework.html</a>. The code for the following example is available on github: <a href="https://github.com/mlex/jerseytest">https://github.com/mlex/jerseytest</a>. </p> <h2>Example REST service</h2> <p>Let's start with a simple example. The following class implements a simple TODO-service. You can get a list of TODOs, add new TODOs and remove a TODO from the list.</p> <pre class="prettyprint lang-java"><br />@Path("/todo")<br />public class TodoResource {<br /> @Context<br /> private TodoService todoService;<br /><br /> @GET<br /> @Produces("text/plain")<br /> public String getTodos() {<br /> return StringUtils.join(todoService.getAllTodos(), ",");<br /> }<br /><br /> @POST<br /> @Consumes("text/plain")<br /> public void addTodo(String newTodo) {<br /> todoService.addTodo(newTodo);<br /> }<br /><br /> @DELETE<br /> @Path("/{todo}")<br /> public void removeTodo(@PathParam("todo") String todoToRemove) {<br /> todoService.removeTodo(todoToRemove);<br /> }<br />}<br /></pre> <p>The instance of <i>TodoService</i> is injected into the REST-resource using a simple <i>SingletonInjectableProvider</i>:</p> <pre class="prettyprint lang-java"><br />@Provider<br />public class TodoServiceProvider extends<br /> SingletonTypeInjectableProvider<Context, TodoService> {<br /><br /> public TodoServiceProvider() {<br /> super(TodoService.class, new TodoService());<br /> }<br />}<br /></pre> <p>To keep the example as simple as possible, the <i>TodoService</i> simply stores the TODOs in a list. In a real-world application, the service would write the todos into a database, of course.</p> <pre class="prettyprint lang-java"><br />public class TodoService {<br /> List<String> todos = new ArrayList<String>();<br /><br /> public List<String> getAllTodos() {<br /> return new ArrayList<String>(todos);<br /> }<br /><br /> public void addTodo(String todo) {<br /> todos.add(todo);<br /> }<br /><br /> public boolean removeTodo(String todo) {<br /> if (todos.remove(todo) == false) {<br /> throw new TodoNotFoundException();<br /> }<br /> }<br />}<br /></pre> </p>The most interesting part of this example (and the part, that demonstrates the need for integration tests on top of unit-tests) is the exception, that is thrown in the removeTodo method. This exception is not catched in the TodoResource. It will be propagated and finally be transformed into a 400-Response by the following exception-mapper:</p> <pre class="prettyprint lang-java"><br />@Provider<br />public class NotFoundMapper implements ExceptionMapper<TodoNotFoundException> {<br /> @Override<br /> public Response toResponse(TodoNotFoundException e) {<br /> return Response.status(Response.Status.BAD_REQUEST)<br /> .entity("TodoNotFoundException").build();<br /> }<br />}<br /></pre> <p>With these classes, our todo-service is ready to use. To check if everything is working, we can use curl:</p><pre class="prettyprint lang-sh"><br />curl -XPOST -H "Content-Type: text/plain" --data "fetch milk" \<br /> http://localhost:8080/mjl-jersey-server/todo<br />curl -XPOST -H "Content-Type: text/plain" --data "call steve" \<br /> http://localhost:8080/mjl-jersey-server/todo<br />curl -XGET http://localhost:8080/mjl-jersey-server/todo<br /># fetch milk,call steve<br /><br />curl -XDELETE http://localhost:8080/mjl-jersey-server/todo/fetch%20milk<br />curl -XGET http://localhost:8080/mjl-jersey-server/todo<br /># fetch milk<br /><br />curl -v -XDELETE http://localhost:8080/mjl-jersey-server/todo/fetch%20milk<br /># ...<br /># < HTTP/1.1 400 Bad Request<br /># ...<br /># TodoNotFoundException<br /></pre> <h2>Testing the REST-service</h2> <p>Now we want to write tests for the todo service. Testing the get- and add-todo methods with the jersey test framework wouldn't be much different from simple unit-tests. The power of the jersey test framework becomes clear, when testing the remove-todo method. When a user wants to delete a non-existent todo, we expect the service to return a 400-response. Ensuring this with standard unit-tests would be hard. The test case using the jersey test framework is quite simple.</p> <pre class="prettyprint lang-java"><br />class TodoResourceTest extends JerseyTest {<br /><br /> public static TodoService todoServiceMock = Mockito.mock(TodoService.class);<br /><br /> @Override<br /> public WebAppDescriptor configure() {<br /> return WebAppDescriptor.Builder(<br /> }<br /><br /> public void shouldReturn400OnNotFoundException() {<br /> String todo = "test-todo";<br /> Mockito.when(todoServiceMock.deleteTodo(todo))<br /> .thenThrow(new NotFoundException());<br /> ClientResponse response = resource().path("todo/"+test-todo")<br /> .delete(ClientResponse.class);<br /> Assertions.assertEquals(Status.BAD_REQUEST, response.getClientStatus());<br /> }<br /><br /> @Provider<br /> public static class MockTodoServiceProvider extends<br /> SingletonTypeInjectableProvider<Context, TodoService> {<br /> public MockTodoServiceProvider() {<br /> super(TodoService.class, todoServiceMock);<br /> }<br /> }<br />}<br /></pre> <p><b>Some explanations:</b><br/>Because we do not want to connect to a external database, the <i>TodoService</i> has to be mocked. This is done by defining a provider, that injects a mocked <i>TodoService</i>. Because we also want to configure the mock-object inside our test, the <i>MockTodoServiceProvider</i> is defined as inner class of the test and the mock-object is stored in a class variable of our test class. </p> <p>The test is configured to use a GrizzlyWebTestContainer. See the last part of this blog-post for advantages and disadvantages of using other containers. The configuration of the test-container is done in the <i>configure()</i> method. <p> <p>In the test method itself, the <i>TodoService</i> mock is instructed to throw a <i>TodoNotFoundException</i>, when the <i>removeTodo()</i> method is called. A <i>WebResource</i> pointing to our test-container is created and a DELETE request is fired. If everything works fine, the result of this request must be a 400 error. And the response-body must contain the reason for the error. </p> <p>In the same way, you can also test other components, like authorization-filters, access-control and response-mappers (Jackson or JAXB) without the need of external environment to be present. Of course, there is also a downside of using this kind of tests: they are rather slow. The on-the-fly setting up and tearing down of the web container is very expensive. Another disadvantage is, that most test-containers use real system ports for their communication (the only exception is the <i>InMemoryContainer</i>, which has other shortcomings). These ports may be blocked by other applications, whath causes the tests to fail. This is a problem, when using helpers like infinitest, where it can happen, that multiple tests are run at the same time. </p> <h2>Integrated Client-Server-Tests</h2> <p>If there is also a java-based client-implementation for the REST-service, this client can be used in jersey tests, too. Our example TODO-service comes with such a client-implementation: </p> <pre class="prettyprint lang-java"><br />public class TodoClient {<br /><br /> public static final String TODO_RESOURCE_PATH = "/todo";<br /><br /> private final String uri;<br /><br /> private final Client client = new Client();<br /><br /> public TodoClient(String uri) {<br /> this.uri = uri;<br /> }<br /><br /> public WebResource resource() {<br /> return client.resource(uri).path(TODO_RESOURCE_PATH);<br /> }<br /><br /> public WebResource resource(String todo) {<br /> return resource().path("/" + todo);<br /> }<br /><br /> public String getAllTodos() {<br /> String todos = resource().get(String.class);<br /> return todos;<br /> }<br /><br /> public void addTodo(String todoToAdd) {<br /> resource().post(todoToAdd);<br /> }<br /><br /> public void removeTodo(String todoToRemove) {<br /> try {<br /> resource(todoToRemove).delete();<br /> } catch (UniformInterfaceException e) {<br /> if (e.getResponse().getClientResponseStatus() == <br /> Response.Status.BAD_REQUEST) &&<br /> "TodoNotFoundException".equals(e.getEntity(String.class))) {<br /> throw TodoNotFoundException();<br /> } else {<br /> throw e;<br /> }<br /> }<br /> }<br />}<br /></pre> <p>The most interesting part of this client is again the <i>removeTodo()</i> method. It not only executes the HTTP request, but also checks if the request failed because the todo to delete did not exist. This is done by checking the response-code and the response-body. This can be used to simplify the jersey test: </p> <pre class="prettyprint lang-java"><br /> private TodoClient todoClient() {<br /> TodoClient todoClient = new TodoClient(getBaseURL());<br /> Whitebox.setInternalState(todoClient, "client", client());<br /> }<br /><br /> @Test(expected = NotFoundException.class);<br /> public void removeTodoShouldThrowNotFoundException() {<br /> final String todo = "test-todo";<br /> Mockito.when(todoServiceMock.removeTodo(todo))<br /> .thenThrow(new NotFoundException());<br /> todoClient().removeTodo(todo);<br /> }<br /></pre> <p>Now this test really cannot be called a unit-test anymore. In these few lines, we check, that the TodoNotFoundException thrown by the TodoServic is correctly converted in a HTTP-Response, that our client understands and converts back to the appropriate TodoNotFoundException. If any of the involved components is changed, without changing affected components, the test will fail. </p> <h2>Tips and Tricks</h2><h3>Decide what type of container to use before writing tests</h3> <p>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. </p> <p>The high-level servlet containers offer the full functionality of a servlet container, automatically injecting instances of HttpServletRequest, ... . If your application relys 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 in tests is, that you don't have direct access to the instances of your resources and (context-)providers. 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 <i>TodoService</i>. </p> <p>Low-level containers on the other hand, allow you to directly modify the ResourceConfig used. Like this, you have access to all instances (resources, providers, filters) used for the rest service. This greatly simplifies mocking. So if you don't rely on the servlet-api, you'll probably go for a low-level container. </p> <h3>Do not use WebAppDescriptor for low-level containers</h3> <p>Althoug possible, I do not recommend using WebAppDescriptors for low-level containers. The reason lies in the method <i>LowLevelAppDescriptor::transform()</i>, that is used to transform a WebAppDescriptor to a LowLevelAppDescriptor, when a low-level container is used. The method simply ignores all non-boolean init-params. Moreover, there is a bug when using the property <i>com.sun.jersey.config.property.packages</i> with multiple (colon-separated) package-names. Even if these shortcomings get fixed, you should not rely on the <i>transform()</i> method. The power of low-level containers lies in the possibility to directly modify the used <i>ResourceConfig</i>, which is only possible when using a <i>LowLevelAppDescriptor</i>. </p> <h3>Speedup jersey tests</h3> <p>Because the JerseyTest base class starts a new web-container before each test, the tests are rather slow. One possibility to speed them up, would be to start a web-container only once per test-suite. A implementation for a base class doing this is included in the example-application. </p> <h3>Extended InMemoryTestContainer</h3><p>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. </p> <p>However the InMemoryTestContainer, coming with the jersey test framework, 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. </p>Michael Lexhttps://plus.google.com/116211833963066507726noreply@blogger.com5tag:blogger.com,1999:blog-6978368526355160507.post-84750727023982194412012-03-08T11:44:00.006+01:002012-03-08T11:44:53.100+01:00Create a list of JAX-RS resources using sed<p>There are things, you can do with sed and things you simply cannot. And there are things, you can do, but should not. The following script belongs probably to the third category. But it was fun to write and the final result is quite beautiful: </p> <pre class="prettyprint lang-sh" style="white-space: pre-wrap; /* css-3 */ white-space: -moz-pre-wrap; /* Mozilla, since 1999 */ white-space: -pre-wrap; /* Opera 4-6 */ white-space: -o-pre-wrap; /* Opera 7 */ word-wrap: break-word; /* Internet Explorer 5.5+ "><br />sed -r 's/\r//g;s/^\s*//g;/@Path.*URI_PATH/d;/final.*URI_PATH/{;/".*"/{;s/.*"(.*)".*/\1/;h;};d;};/^@Path[^P]/{;s/.*@Path\("(.*)"\).*/\1/;H;d;};/public/{;g;/\n@/{;s/^([^\n]*)\n?(.*)?\n(@[^\n]*)\n?(.*)?/\3\t\1\4\n\2/;p;};};/\/\*\*/{;g;s/^([^\n]*)\n.*$/\1/;s/$/\n!/;h;d;};/\*\//{;g;s/\n!$//;h;d;};/^\*/{;/^\*\s*(@.*)?$/{;g;s/\n!$//;h;};x;/\n!$/{;s/\n!$//;x;s/^\*\s*/\t\t/;H;g;s/$/\n!/;};x;d;};/@(GET|PUT|POST|DELETE)/{;H;d;};d' *.java<br /></pre> <p>So what is this piece of cryptic code supposed to do? It takes some java-files, scans them for JAX-RS annotations (@Path, @GET, ...) and prints a small overview of all REST-resources, that are defined in these files together with a small description, taken from the java-comments. Take the following example-implementation of a REST-Service: </p> <pre class="prettyprint lang-java"><br />@Path(BlogPostResource.URI_PATH)<br />class BlogPostResource {<br /><br /> public final static String URI_PATH = "/blogposts";<br /><br /> /**<br /> * Create a new blog post<br /> */<br /> @POST<br /> public Response postNewPost() {<br /> //...<br /> }<br /><br /> /**<br /> * Get blog post with given id<br /> */<br /> @GET<br /> @Path("/{id}")<br /> public Response getPost(@PathParam("id") String id) {<br /> //...<br /> }<br /><br /> /**<br /> * Update the blogpost with given id.<br /> * Multiline comments are possible, too.<br /> * <br /> * @param id<br /> * the id of the blog post to update<br /> */<br /> @PUT<br /> @PATH("/{id}")<br /> public Response updatePost(@PathParam("id") String id) {<br /> //...<br /> }<br />}<br /></pre> <p>This is what the script will return: </p> <pre><br />@POST /blogposts<br /> Create a new blog poast<br />@GET /blogposts/{id}<br /> Get blog post with given id<br />@PUT /blogposts/{id}<br /> Update the blogpost with given id.<br /> Multiline comments are possible, too.<br /></pre> <p>There are some assumptions to be made for the script to work<br />correctly: <ol><li>The base-path of each class implementing a rest-resource must be red in a static class-variable "URI_PATH". This variable must be defined before any other annotated methods. </li><li>The @GET (resp @POST, @PUT, @DELETE) annotations must always be above the @Path annotation.</li><li>Every rest-implementing method must have a javadoc-comment starting with /** (even if it is empty).</li><li>The javadoc comment must always come before the annotations</li></ol></p> <p>This said, here is the less cryptic (and commented) code of the sed-script: <pre class="prettyprint lang-sh"><br />sed -r '<br /># before we start, remove all carriage-returns (i really hate these)<br />s/\r//g<br /><br /># also remove whitespace at the beginning of lines<br />s/^\s*//g<br /><br /># the @Path annotation of the class itself is always used in<br /># conjunction with the static class-variable URI_PATH, which we catch<br /># separately, so we can safely ignore this line<br />/@Path.*URI_PATH/ d <br /><br /># the static class-variable holds the global path to this resource. we<br /># put it in hold-space. this path will stay in hold space (more exactly<br /># in the first line of the hold space.<br />/final.*URI_PATH/ {<br /> /".*"/ {<br /> s/.*"(.*)".*/\1/<br /> h<br /> }<br /> d<br />}<br /><br /># this is a path-annotation of a method receiving rest-requests. the<br /># path specified is appended to the hold space (the [^P] is to not get<br /># confused by lines starting with @PathParam.<br />/^@Path[^P]/ {<br /> s/.*@Path\("(.*)"\).*/\1/<br /> H<br /> d<br />}<br /><br /># a new public method (or field) is declared in this line, so we have to<br /># check if the hold space content is<br /># describing a rest-resource. this is the case if some line starting<br /># with a @ is found (we assume, that no comments start with a @)<br />/public/ {<br /> g<br /> /\n@/ {<br /> # now we have to print a description of the rest-resource<br /> # the first line in hold space is the base path (from the<br /> # @Path annotation of the class itself). the next lines are the<br /> # description from the comments. the line starting with an @ is the<br /> # http-method-specifier (for example @GET). and after this<br /> # line comes the path specified by the @Path-annotation for the<br /> # method (which is not required for all methods, therefor we put a ?<br /> # to this part). unfortunately, there exist also methods<br /> # without a describing comment, so the second part gets a ?, too<br /> s/^([^\n]*)\n?(.*)?\n(@[^\n]*)\n?(.*)?/\3\t\1\4\n\2/<br /> p<br /> }<br />}<br /><br /># a new comment starts ...<br />/\/\*\*/ {<br /> # in any case, we have to clean up the hold space. only the<br /> # first line may remain<br /> g<br /> s/^([^\n]*)\n.*$/\1/<br /><br /> # now we add a marker to the hold space, to tell the methods<br /> # parsing the comments, that we are at the beginning of a comment<br /> s/$/\n!/<br /> <br /> # put everything in the hold-space and delete the pattern space,<br /> # so that subsequent checks are not disturbed<br /> h<br /> d<br />}<br /><br /># when we reach the end of a comment, the marker "!" must be removed<br />/\*\// {<br /> g<br /> s/\n!$//<br /> h<br /> d<br />}<br /><br /># here we are inside a comment<br />/^\*/{<br /> # if this comment-line is empty or starting with a @ (which means<br /> # we are inside the parameter-description part), and the "!"<br /> # marker is still in the hold space, we remove the marker<br /> /^\*\s*(@.*)?$/ {<br /> g<br /> s/\n!$//<br /> h<br /> }<br /><br /> # now we check if the last character in the hold-space is a<br /> # "!". this tells us, if we are at the beginning of a (perhaps<br /> # multiline-)comment, or if we are already in the part describing<br /> # parameters (which is of no interest to us)<br /> x<br /> /\n!$/ {<br /> # first, we remove the "!" character. it will be re-added,<br /> # when we are finished reading this comment-line<br /> s/\n!$//<br /> <br /> # next we refetch the current pattern line (which we exchanged<br /> # with hold-space before)<br /> x<br /> <br /> # remove the leading stars and whitespaces<br /> s/^\*\s*/\t\t/<br /><br /> # then add the line to hold space<br /> H<br /> <br /> # and finally re-add our marker "!"<br /> g<br /> s/$/\n!/<br /> }<br /><br /> # before we finish, we have to re-exchange hold- and pattern<br /> # space, because we swapped them before the \n! check<br /> x<br /> d<br />}<br /><br /># this is an easy part: if a http-method annotation is present, push<br /># it to the hold-space<br />/@(GET|PUT|POST|DELETE)/ {<br /> H<br /> d<br />}<br /><br /># we finally delete all lines, so that they are not printed by sed in<br />#default mode (if the script is called with "sed -n", this is not needed)<br />d' *.java<br /></pre> <p>By the way, this is the command, i used to create the cryptic version out of the&nbsp; rather lengthy commented one: </p> <pre class="prettypring lang-sh"><br />sed -n '/#/ d; s/\s*//g; /^$/ d; 1h; 1!H; ${g;s/\n/;/g;p}'<br /></pre>Michael Lexhttps://plus.google.com/116211833963066507726noreply@blogger.com0