When writing or consuming REST APIs there are many excellent options to choose from. When it comes to testing parts of an API in isolation the situation is noticeably worse, though, especially in the Jersey ecosystem. Nevertheless is it useful to not only distinguish between unit and integration tests but also be able to apply both concepts when developing and testing REST APIs.
In this blog entry we will explore how unit tests relate to REST APIs in general, which tools the Jersey framework provides in this regard, the pitfalls to deal with when using them, and finally, how we may overcome its shortcomings.
Table Of Contents
- Spring, anyone?
- Unit Testing REST
- Jersey Test Framework
- Custom Controller Tests
Yes, we, too, have heard of Spring. The abstractions it provides are arguably superior to Jersey’s more imperative style. This is especially true when it comes to easy testing where Spring shines, whereas Jersey does not.
Even though Jersey does provide a dedicated test framework you have to do all the work of setting it up and wiring it together yourself. If using custom filters, features and/or injection providers this becomes a tedious affair at best, and error prone or ignored at worst.
So why try making Jersey testing work regardless? Sometimes switching to Spring is not viable, e.g. for legacy projects. Some people simply like Jersey better. Having the option to write proper unit tests in Jersey is a good thing regardless of the motivation.
Unit Testing REST
Let us assume we have a very basic Jersey controller for a RESTlike ressource:
If we want to write unit tests for our controller we might trivially do so like this:
Note that this is a unit test for our Java controller method and not in fact for our REST API method.
When checking the result of our call its representation is a Java class and not a JSON string as indicated by our controller. At this point we are unit testing our implementation internals as opposed to our public contract.
To put it more bluntly we have not tested our actual REST API at all.
Jersey Test Framework
Luckily, Jersey does provide its own testing framework, including a slim in-memory application container.
Let’s try again:
Now we can make assertions about the API our actual consumers will use, and ensure our contract matches the actual responses going out.
Note that, while we did fire up an actual mini-application container, we restricted ourselves to the one controller we wanted to test. Thus we can still call this a unit test, confined to the actual unit under test.
Contrast this with e.g. RESTassured which allows testing your REST API in a very similar fashion but without allowing you to confine yourself to only parts of your application, making it more of an integration test suite.
So, it looks Jersey makes it all easy enough for us. For simple code such as above this may hold; if making use of more of Jerseys features the presented method does scale poorly, unfortunately.
There are some cases where things start to fall apart, among them mocks and providers.
Mocking parts of an application is an essential technique to ensure manageable and meaningful tests. In our example we probably want to mock our service since its logic not the thing we want to test here; we rather assume this part of our application is working as intended and concentrate on the code the test is being written for:
If we test Java code wiring this up is straightforward:
Getting Jersey to pick this up requires the following in addition:
This means a lot of boilerplate to accommodate a standard workflow. While setting up tests this way is very much possible it does result in a lot of duplicate code that does not directly contribute to our test goals but needs to be maintained regardless.
One of the powerful features Jersey provides is customised injection of parameters into methods managed by Jersey’s lifecycle. In the following snippet we let Jersey handle the controller argument annotated as @User and have it inject a proper object at the call site whenever we need it. The actual setup of the @User instance is defined by a so called Provider that we register with Jersey.
To ensure this works in our unit tests we have to actively register our provider implementation or preferably bind to a mock instance:
This, too, necessitates additional code to make it work within the Jersey Test Framework, rendering testing your API properly more verbose and brittle than writing regular unit tests.
Custom Controller Tests
Taking the above into account may make testing REST resources look like too much of a hassle to do with Jersey. Which in turn may lead to many developers just not doing it.
Luckily, Jersey allows us to hide most of that boilerplate away, requiring us to write our test setups only slightly differently than what we are used to with regular Java tests.
Applied to our example controller such a test might look similar to the following:
It is readily apparent that not having to set up the Jersey container by ourselves improves readability and maintainability of our test. All the complexity is provided by and hidden behind the generic type of our extended class.
Mocking our internal EchoService is as terse as with a regular Java class; the only difference being that we retrieve an already mocked parameter instead of passing it on in the first place. The actual definition of the mocked behaviour is done as usual.
The actual tests can then be defined as demonstrated earlier – testing REST functionality as opposed to Java code.
Combining the terseness of a unit test with the capabilities of Jersey’s test framework may look like we are having our cake and eating it, too.
It is made possible by extending the JerseyTest class provided by Jersey’s test framework and automating all of the tedious setup it requires.
At the moment this means conforming to a set of assumptions in order to work properly:
- controller dependencies must be provided explicitly – no injection via annotation
- controller dependencies can only be accessed by type
- test constructors should be explicitly annotated, defaulting to the constructor with the most arguments
- constructor arguments are always mocked automatically
- filters and providers are always registered for all controller tests
In turn this provides us with the following facilities:
- automatic setup of the Jersey test container with the controller under test and classes relevant to the Jersey lifecycle
- automatic mocking of all constructor parameters
- convenient accessor method for constructor parameters
The actual implementation is rather concise and straightforward, too:
Since Jersey is a tad picky when it comes to mixing versions of its various sub-projects here is the `pom.xml` used throughout the examples above.