I had previously written an article Integration Testing with MockWebServer that explained a way to write integration tests for a web application using WebClient and MockWebServer (okhttp). The intention was to write an integration test that did not touch the inside of the code, but only the edges. It was not completely successful. The tests overrode the WebClient, and so did not cover the configuration of the WebClient (which could be incorrectly configured). Spring Boot 2.2.6 introduced the @DynamicPropertySource which allows you to insert the MockWebServer url into the properties at runtime.
This lets you test your WebClient setup as well. Better still, it enables you to test the token caching functionality that comes built into WebClient, but we won’t get to that today.
2. MockWebServer Dependencies
To use MockWebServer, you need two dependencies. Shown below as Gradle imports:
3. Test Project
We will be using Spring Boot version 2.6.3 with Gradle wrapper version 7.3.3 and Java 11. Our project will include Spring Security with OAuth2 Client Credentials, Actuator, Spring Web, JUnit 5 and Webflux, and some other common dependencies. It is a simple pass-through API that hits a backend and returns the response from it. If you want, you can skip to my GitHub repo with the build.gradle file and example code.
4. Writing the Tests
MockWebServer will generate a url, which we will insert into our application properties using @DynamicPropertySource.
We are also choosing to not require the MockMvc to pass a token to our application, because that concern is better tested after we deploy our application.
Here is the test code for handling 200 responses for GET and POST and a 500 response:
5. Explaining the Tests
The first thing to notice is that there are only 2 annotations needed, compared to the 7 annotations in my previous blog. The @ActiveProfiles annotation is optional depending on how many properties you have in your local spring profile. In this example the url was the only property, and it was injected with the @DynamicPropertySource functionality.
In JUnit 5 @SpringBootTest spins up the context. It includes the @ExtendWith(SpringRunner.class) annotation, so you do not need to add that.
The @AutoConfigureMockMvc(addFilters = false) annotation sets up the MockMvc functionality so you can autowire and use the MockMvc. Setting the addFilters to false turns off security so you don’t have to pass an auth token to call your endpoint. As of Spring 5.3 (Spring Boot 2), the @WebAppConfiguration annotation is no longer needed for MockMvc to work.
The MockWebServer is started in the @BeforeAll setup and stopped in the @AfterAll and is set to a field. This means it starts before any tests run, and is not restarting between tests, just like the Spring context.
The @DynamicPropertySource method takes in the registry of properties, and lets you add or override properties. I am overriding the “base-url” property with the value of the MockWebServer url and random port (e.g. http://localhost:2345). The “/” shows I am not adding a path. This executes after the MockWebServer starts running, but before the tests start running which allows us to insert the generated urls.
With the tests for the GET calls, we are executing the call with a simple MockMvc implementation and verifying the response in a separate method so we can separate the Arrange, Act and Assert stages of the tests. We also created a helper method to verify the backend was called correctly. You can see that our application code added the /api/clock/alarms path to the end of the base url when it calls the backend.
The test for the POST endpoint shows that our application called the backend POST endpoint correctly. You can see that when we pass the request into the assertBackendServerWasCalledCorrectlyForPOST() helper method, we use the mockServer.takeRequest(5L, TimeUnit.SECONDS) method.
This collects requests for 5 seconds before continuing and is necessary for POST tests. Adding this timeout for all your mockServer.takeRequest() methods is a good idea so the tests will fail fast when they fail, instead of hanging.
Getting the body from the request to the backend can be done by reading the byte string of the body and converting it to a string. The resulting format is different than you might expect. It has the word “text” in square brackets with the JSON body:
One challenge is that if there is any problem with your test setup or mocks you are likely to get a blocking timeout error instead of a useful error, and the debugger can be difficult to follow. Because of this, I recommend you limit the number of integration tests to as few as possible, only testing concerns that cannot be tested with micro tests. For instance, what your response body format looks like when you receive a 500 from the backend.