Better integration tests with WireMock
No matter if you follow the classical test pyramid or one of the newer approaches like the Testing Honeycomb you should start writing integration tests at some point during development.
There are different types of integration tests you can write. Starting with persistence tests, you can check the interaction between your components or you can simulate calling external services. This article will be about the latter case.
Let us start with a motivating example before talking about WireMock.
The ChuckNorrisFact service
The complete example can be found on GitHub.
You might have seen me using the Chuck Norris fact API in a previous blog post. The API will serve us as an example for another service that our implementation depends on.
We have a simple ChuckNorrisFactController
as the API for manual testing. Next to the “business” classes there is the ChuckNorrisService
that does the call to the external API. It uses Spring’s RestTemplate
. Nothing special.
What I have seen many times are tests that mock the RestTemplate and return some pre-canned answer. The implementation could look like this:
1 2 3 4 5 6 7 8 9 | @Service public class ChuckNorrisService{ ... public ChuckNorrisFact retrieveFact() { ResponseEntity<ChuckNorrisFactResponse> response = restTemplate.getForEntity(url, ChuckNorrisFactResponse. class ); return Optional.ofNullable(response.getBody()).map(ChuckNorrisFactResponse::getFact).orElse(BACKUP_FACT); } ... } |
Next to the usual unit tests checking for the success cases there would be at least one test covering the error case, i.e. a 4xx or 5xx status code:
01 02 03 04 05 06 07 08 09 10 11 12 | @Test public void shouldReturnBackupFactInCaseOfError() { RestTemplate mockTemplate = mock(RestTemplate. class ); ResponseEntity<ChuckNorrisFactResponse> responseEntity = new ResponseEntity<>(HttpStatus.SERVICE_UNAVAILABLE); when(mockTemplate.getForEntity(url, ChuckNorrisFactResponse. class )).thenReturn(responseEntity); var service = new ChuckNorrisService(mockTemplate, url); ChuckNorrisFact retrieved = service.retrieveFact(); assertThat(retrieved).isEqualTo(ChuckNorrisService.BACKUP_FACT); } |
Doesn’t look bad, right? The response entity returns a 503 error code and our service will not crash. All tests are green and we can deploy our application.
Unfortunately, Spring’s RestTemplate does not work like this. The method signature of getForEntity
gives us a very small hint. It states throws RestClientException
. And this is where the mocked RestTemplate differs from the actual implementation. We will never receive a ResponseEntity
with a 4xx or 5xx status code. The RestTemplate will throw a subclass ofRestClientException
. Looking at the class hierarchy we can get a good impression of what could be thrown:
Therefore, lets see how we can make this test better.
WireMock to the rescue
WireMock simulates web services by starting a mock server and returning answers you configured it to return. It is easy to integrate into your tests and mocking requests is also simple thanks to a nice DSL.
For JUnit 4 there is a WireMockRule
that helps with starting an stopping the server. For JUnit 5 you will have to do it yourself. When you check the example project you can find the ChuckNorrisServiceIntegrationTest
. It is a SpringBoot test based on JUnit 4. Let’s take a look at it.
The most important part is the ClassRule
:
1 2 | @ClassRule public static WireMockRule wireMockRule = new WireMockRule(); |
As mentioned before, this will start and stop the WireMock server. You could also use the rule as normal Rule
to start and stop the server for each test. For our test this isn’t necessary.
Next, you can see several configureWireMockFor...
methods. These contain the instructions for WireMock when to return what answer. Splitting the WireMock configuration into several methods and calling them from the tests is my approach to using WireMock. Of course you could set up all possbile requests in an @Before
method. For the success case we do:
1 2 3 4 5 | public void configureWireMockForOkResponse(ChuckNorrisFact fact) throws JsonProcessingException { ChuckNorrisFactResponse chuckNorrisFactResponse = new ChuckNorrisFactResponse( "success" , fact); stubFor(get(urlEqualTo( "/jokes/random" )) .willReturn(okJson(OBJECT_MAPPER.writeValueAsString(chuckNorrisFactResponse)))); } |
All methods are imported statically from com.github.tomakehurst.wiremock.client.WireMock
. As you can see, we stub an HTTP GET to a path /jokes/random
and return a JSON object. TheokJson()
method is just shorthand for a 200 response with JSON content. For the error case the code is even more simple:
1 2 3 4 | private void configureWireMockForErrorResponse() { stubFor(get(urlEqualTo( "/jokes/random" )) .willReturn(serverError())); } |
As you can see, the DSL makes it easy to read the instructions.
Having WireMock in place we can see that our previous implementation does not work since the RestTemplate throws an exception. Therefore, we gotta adjust our code:
1 2 3 4 5 6 7 8 | public ChuckNorrisFact retrieveFact() { try { ResponseEntity<ChuckNorrisFactResponse> response = restTemplate.getForEntity(url, ChuckNorrisFactResponse. class ); return Optional.ofNullable(response.getBody()).map(ChuckNorrisFactResponse::getFact).orElse(BACKUP_FACT); } catch (HttpStatusCodeException e){ return BACKUP_FACT; } } |
This already covers WireMock’s basic use-cases. Configure an answer for a request, execute the test, check the results. It’s as simple as that.
Still, there is one problem you will usually encounter when you run your tests in a cloud environment. Let’s see what we can do.
WireMock on a dynamic port
You might have noticed that the integration test in the project contains anApplicationContextInitializer
class and that its @TestPropertySource
annotation overwrites the URL of the actual API. That is because I wanted to start WireMock on a random port. Of course you can configure a fixed port for WireMock and use this one as hard-coded value in your tests. But if your tests are running on some cloud providers infrastructure you cannot be sure that the port is free. Therefore, I think a random port is better.
Still, when using properties in a Spring application we have to pass the random port somehow to our service. Or, as you can see in the example, overwrite the URL. That is why we use the ApplicationContextInitializer
. We add the dynamically assigned port to the application context and then we can refer to it by using the property${wiremock.port}
. The only disadvantage here is that we now have to use a ClassRule. Else, we couldn’t access the port before the Spring application is being initialized.
Having solved this problem, let’s take a look at one common problem when it comes to HTTP calls.
Timeouts
WireMock offers many more possibilities for responses than just simple answers to GET requests. Another test case that is often forgotten is testing timeouts. Developers tend to forget to set timeouts on the RestTemplate
or even on URLConnections
. Without timeouts both will wait for an infinite amount of time for responses. In the best case you will not notice, in the worst case all your threads wait for a response that will never arrive.
Therefore, we should add a test that simulates a timeout. Of course, we can also create a delay with e.g. a Mockito mock, but in that case we would guess again how the RestTemplate behaves. Simulating a delay with WireMock is pretty easy:
1 2 3 4 5 6 7 | private void configureWireMockForSlowResponse() throws JsonProcessingException { ChuckNorrisFactResponse chuckNorrisFactResponse = new ChuckNorrisFactResponse( "success" , new ChuckNorrisFact(1L, "" )); stubFor(get(urlEqualTo( "/jokes/random" )) .willReturn( okJson(OBJECT_MAPPER.writeValueAsString(chuckNorrisFactResponse)) .withFixedDelay(( int ) Duration.ofSeconds(10L).toMillis()))); } |
withFixedDelay()
expects an int value representing milliseconds. I prefer using Duration or at least a constant that indicates that the parameter represents milliseconds without having to read the JavaDoc every time.
After setting a timeout on our RestTemplate
and adding the test for the slow response we can see that the RestTemplate throws a ResourceAccessException
. So we can either adjust the catch block to catch this exception and the HttpStatusCodeException
or just catch the superclass of both:
1 2 3 4 5 6 7 8 | public ChuckNorrisFact retrieveFact() { try { ResponseEntity<ChuckNorrisFactResponse> response = restTemplate.getForEntity(url, ChuckNorrisFactResponse. class ); return Optional.ofNullable(response.getBody()).map(ChuckNorrisFactResponse::getFact).orElse(BACKUP_FACT); } catch (RestClientException e){ return BACKUP_FACT; } } |
Now we have nicely covered the most common cases when doing HTTP requests and we can be sure that we are testing close to real world conditions.
Why not Hoverfly?
Another choice for HTTP integration tests is Hoverfly. It works similar to WireMock but I have come to prefer the latter. The reason is that WireMock is also quite useful when running end-to-end tests that include a browser. Hoverfly (at least the Java library) is limited by using JVM proxies. This might make it faster than WireMock but when e.g. some JavaScript code comes into play it does not work at all. The fact that WireMock starts a webserver is very useful when your browser code also calls some other services directly. You can then mock those with WireMock, too, and write e.g. your Selenium tests.
Conclusion
I hope this article could show you two things:
- the importance of integration tests
- that WireMock is pretty nice
Of course, both topics could fill many more articles. Still, I wanted to give you a feeling of how to use WireMock and what it is capable of. Feel free to check their documentation and try many more things. As an example, testing authentication with WireMock is also possible.
Published on Java Code Geeks with permission by Ronny Braunlich, partner at our JCG program. See the original article here: Better integration tests with WireMock Opinions expressed by Java Code Geeks contributors are their own. |