This post is a follow up post to a few that I have written recently relating to writing a HATEOAS service and handling exceptions with Spring (Applying HATEOAS to a REST API with Spring Boot and Global exception handling with @ControllerAdvice). Now that we have looked through setting up a service and how to add some error handling via controller advice, it’s probably now worth writing some tests to ensure that it works as expected.
There are a few extra dependencies that need to be added to allow the tests to be written. I have excluded the dependencies for setting up the HATEOAS service as they can be found in the previous post here.
spring-boot-starter-test is the default requirement for most tests when using Spring Boot and
hamcrest-core allows us to assert the JSON returned in responses more easily.
Before we look at the test itself, lets have a look at the code that we are trying to test. The short story of the example below is that it is a HATEOAS service which returns a response that can contain an object plus links to relevant resources. For more information about what is going on refer to the previous post mentioned earlier (added again here if you still need to look at it).
Now we can write the test code.
There are a few comments scattered throughout the test above, but lets have a proper look into it. Firstly we have some of the general setup. The
SpringRunner is used and
@WebMvcTest marks the controller that is being tested.
@MockBean allows the
PersonRepository to be mocked out as is not important to this specific test and
ObjectMapper allows you to convert objects into JSON so that they can be passed to the controller as if you were manually testing it yourself.
MockMvc is used to send requests to the controller being tested (the controller is spun up at the start of the test) and the response of the call can be asserted to assure that the code is running correctly.
There are a lot of static methods in this example which have been statically imported to make the code look tidier, unfortunately this can make it a little unclear where they are coming from, another version of the code with imports can be found here.
Below I have taken a snippet of the code in the test to focus on.
Here we have a test which is checking if the JSON returned from a GET request is correct. The first line of the test mocks out the
findById repository method and returns the person that was created in the setup method. The following line sends the request.
get specifies the type of request and the string or URI passed into it tells it where to send the call to. Due to a lot of reused code I decided to split a large chunk of the assertion code out into a separate method. As I decided to split this code out the response has been stored in the
ResultActions object where it is then checked that it’s response code was
200 OK and then
verifyJson goes on to ensure the JSON is correct.
jsonPath method is very handy, allowing you to check the JSON one property at a time. The
is method comes from the
hamcrest-core dependency and when used in conjunction with
jsonPath makes for a very tidy way of checking the correctness of the returned JSON. If you decided to not to do it this way, you might need to store a long string to represent the JSON and basically compare two strings together, I can personally tell you that doing it this way is a tad annoying. Below is the JSON returned when the test is ran and we can use this to provide a very brief explanation into how
Separate a line for closer inspection.
So this is saying does the
firstName property of the
Person object (named
person) have the value of
person.getFirstName, if so then this line passes.
And a slightly different example.
.andExpect(jsonPath("links.rel", is("people"))) .andExpect(jsonPath("links.href", is(BASE_PATH)))
links being an array you need to retrieve values from it as such. Therefore this snippet says; get the first object in the
links array, get the property
href and check their values. These links are what are included due to it being a HATEOAS service. The code above is repeated for the remaining links. If you have noticed the URI’s included in the response include “localhost” etc and therefore the string it is being compared to needs to also needs to contain it. The reason I mention this is because the URI passed into the
get method (or any of the other verbs) do not need to receive a whole URI and therefore can take in
/people/1 instead of
http://localhost/people/1. But if you tried to do the same inside of the
is methods they would fail instead. Just something I think is worth looking out for as it might trip you up if you are not careful.
Next we will look at another test snippet which focuses on the response returned when an exception is thrown. Due to the exception being handled in
PersonControllerAdvice some extra configuration needs to be applied (which caused some weird things to happen which I’ll get into later).
The test case
This time round the mocked repository method will return
Optional.empty which in turn causes a exception to be thrown inside the controller’s GET method. If everything is setup correctly, this exception is handled by the
@ExceptionHandler inside of
PersonControllerAdvice, converted into a
ResponseEntity and returned. Below is the JSON included in the response.
It is then checked in the same way all the other tests are. The
$ represents first value of the unnamed array.
Oh, I should probably go over that configuration I mentioned a minute ago as well. The
mockMvc object needs to be setup to allow classes annotated with
@ControllerAdvice to be used (in this case
PersonControllerAdvice). If you do not require this extra configuration (or the others available) then just injecting in
@Autowired will suffice.
setControllerAdvice can take in an array of objects allowing you to enable multiple
@ControllerAdvice classes to use with your test.
I also mentioned that some weird things happened when I added this configuration. Depending on whether I setup
mockMvc manually using the snippet above or injected it using
@Autowired the JSON returned in the response would differ slightly. This lead to the JSON returned in the test being different from the output of manual testing, which is not the best outcome for a test. Below is the JSON with the manual configured
mockMvc followed by the injected version.
As you can see, the difference lies in the JSON representing the links. Where in one version it is an array called
links and in the other it is an object called
_links with properties representing each link.
The correct code to test the JSON from the second snippet is as follows. Note the difference in the path passed into
jsonPath due to it being an object instead of an array.
Personally I have not tried to look into the code that could cause this and therefore do not know whether it is a bug/oversight, or if I did something wrong myself… Either way it is worth knowing that this could happen.
Unfortunately this left my test output looking different to my production code, a possible solution for my scenario is splitting the test into separate classes where one uses an injected
mockMvc and the other tests the exception handling with the configured
mockMvc. This should allow the JSON to match the output of the production code in more situations.
In conclusion, writing tests for a HATEAOS service is not very different from testing a REST API and just requires a few more asserts to ensure that the links are returned correctly. We also had a brief look at using
jsonPath to help make verifying the output JSON easier and how we can configure the test to allow for error handling to be checked when a
@ControllerAdvice class is used.
The code used in this post can be found on my GitHub.