How to use Mock Server to End to End Test Any WebClient Calls in Spring Boot Webflux
Aug 2020
The source code for this post can be found on Github.
Mock Server is a really simple and straightforward way to actually let your application make downstream calls and intercept them. That level of abstraction is really nice to have, and gives at least me much more confidence that my code is actually working in a microservices environment.
There are a few different ways to run MockServer with junit, depending on the junit version and how much manual work you want to do. I'll go with the most portable setup for this tutorial.
The Service, Using WebClient
I'm going to reuse code from my last blog post on mocking dependencies and unit testing in webflux. If you recall, we had a really simple service with basically no logic:
package com.nickolasfisher.testing.service;
import com.nickolasfisher.testing.dto.DownstreamResponseDTO;
import com.nickolasfisher.testing.dto.PersonDTO;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Flux;
@Service
public class MyService {
private final WebClient webClient;
public MyService(WebClient webClient) {
this.webClient = webClient;
}
public Flux<DownstreamResponseDTO> getAllPeople() {
return this.webClient.get()
.uri("/legacy/persons")
.retrieve()
.bodyToFlux(DownstreamResponseDTO.class);
}
}
To write a test for this, let's first get mock server setting up properly. You will want to add the mock server dependency to your pom.xml if you're using maven, or your build.gradle if you're using gradle:
<dependency>
<groupId>org.mock-server</groupId>
<artifactId>mockserver-netty</artifactId>
<version>5.11.1</version>
</dependency>
And create the test class somewhere in src/test/.... We will start and stop mock server as boilerplate:
public class MyServiceTest {
private ClientAndServer mockServer;
@BeforeEach
public void setupMockServer() {
mockServer = ClientAndServer.startClientAndServer(2001);
}
@AfterEach
public void tearDownServer() {
mockServer.stop();
}
}
I picked a static port in this case, but it's probably more robust to have java find you an available TCP port. Since we're using Spring Boot, you should check out SocketUtils to find an available TCP port for you.
There are two things we would want to assert in a happy path test: first, that we are actually making the request and second, we are deserializing the response properly and it is making it into the Flux. One way to do this is by forcing the downstream call to block for us:
public class MyServiceTest {
private ClientAndServer mockServer;
private MyService myService;
private static final ObjectMapper serializer = new ObjectMapper();
@BeforeEach
public void setupMockServer() {
mockServer = ClientAndServer.startClientAndServer(2001);
myService = new MyService(WebClient.builder()
.baseUrl("http://localhost:" + mockServer.getLocalPort()).build());
}
@AfterEach
public void tearDownServer() {
mockServer.stop();
}
@Test
public void testTheThing() throws JsonProcessingException {
String responseBody = getDownstreamResponseDTOAsString();
mockServer.when(
request()
.withMethod(HttpMethod.GET.name())
.withPath("/legacy/persons")
).respond(
response()
.withStatusCode(HttpStatus.OK.value())
.withContentType(MediaType.APPLICATION_JSON)
.withBody(responseBody)
);
List<DownstreamResponseDTO> responses = myService.getAllPeople().collectList().block();
assertEquals(1, responses.size());
assertEquals("first", responses.get(0).getFirstName());
assertEquals("last", responses.get(0).getLastName());
mockServer.verify(
request().withMethod(HttpMethod.GET.name())
.withPath("/legacy/persons")
);
}
private String getDownstreamResponseDTOAsString() throws JsonProcessingException {
DownstreamResponseDTO downstreamResponseDTO = new DownstreamResponseDTO();
downstreamResponseDTO.setLastName("last");
downstreamResponseDTO.setFirstName("first");
downstreamResponseDTO.setSsn("123-12-1231");
downstreamResponseDTO.setDeepesetFear("alligators");
return serializer.writeValueAsString(Arrays.asList(downstreamResponseDTO));
}
}
We are verifying that at least part of the response got loaded into our POJO properly with:
assertEquals(1, responses.size());
assertEquals("first", responses.get(0).getFirstName());
assertEquals("last", responses.get(0).getLastName());
And we are verifying the request was made with:
mockServer.verify(
request().withMethod(HttpMethod.GET.name())
.withPath("/legacy/persons")
);
Note that you would never want to use those blocking methods in any production code, but for testing it helps simplify our life and gives us a high degree of confidence we are doing things properly.
As a reminder, you can checkout and use this code on Github.
Nick Fisher is a software engineer in the Pacific Northwest. He focuses on building highly scalable and maintainable backend systems.