How to test any HTTP Client of your Spring Boot Application with MockServer


In an integration test, we want to test the interaction of several components of our application. However, as soon as one of these components communicates with a 3rd party service via HTTP, this can present us with a challenge. To be independent of this service, we have to mock it. In this blog post, I will show you how to do that with the help of MockServer.

What is MockServer?

MockServer is a tool to mock any service that communicates via HTTP(S). In integration tests, we can use it to decouple our code from the real service we want to integrate with. By doing so, our tests will be independent of the availability of the real service, and we can test the behavior of our code in different success or error scenarios. To configure the MockServer we set expectations containing a request matcher and an action. When MockServer receives a request, it checks for a matching request matcher, by comparing properties like HTTP method, request path, headers, cookies, etc. If there is a match, it will respond according to the action. For example a success response or an error.

The Example

In this blog post, we will extend the test suite of my previous post “How to test the REST Clients of your Spring Boot Application with @RestClientTest”. There we created a client for the Star Wars API by using Spring’s RestClient. We also tested this client in isolation by leveraging the @RestClientTest test slice. Today we want to write an integration test, that spans the complete application from the controller to the repository.

Here is an overview of the involved classes:

StarWarsCharacterRepository.java
@Repository
public class StarWarsCharacterRepository {

    private final RestTemplate restTemplate;

     public StarWarsCharacterRepository(RestTemplateBuilder restTemplateBuilder,
                                       @Value("${swapi.baseUrl}") String baseUrl) {
        this.restTemplate = restTemplateBuilder.rootUri(baseUrl).build();
    }

    public Optional<Character> findById(Long id) {
        //...
    }
}

  • The base URL of the repository is configured with a property.
    This will be handy in the integration tests we are going to write.

CharacterController.java
@RestController
@RequestMapping("/characters")
public class CharacterController {

    private final StarWarsCharacterRepository repository;

    public CharacterController(StarWarsCharacterRepository repository) {
        this.repository = repository;
    }

    @GetMapping(value = "/{id}", produces = MediaType.APPLICATION_JSON_VALUE)
    public Character findById(@PathVariable Long id) {
        return repository.findById(id).orElseThrow(() -> new CharacterNotFoundException(id));
    }
}
  • This is the endpoint we are going to test.
    It returns the found Character as JSON or a 404 response if non is found.

Test Setup

Before we can start to implement the test, we first need to add MockServer as a dependency to the project.

pom.xml
<dependency>
    <groupId>org.mock-server</groupId>
    <artifactId>mockserver-spring-test-listener</artifactId>
    <version>5.14.0</version>
</dependency>
...
  • We add the mockserver-spring-test-listener dependency.
    This includes a TestExecutionListener for our tests, as well as MockServer itself and a MockServerClient that is used to set expectations.

The Test

Let’s start by implementing the skeleton of our test class:

CharacterControllerIT.java
@SpringBootTest
@AutoConfigureMockMvc
@MockServerTest({"swapi.baseUrl=http://localhost:${mockServerPort}"})
class CharacterControllerIT {

    @Autowired
    private MockMvc mockMvc;

    private MockServerClient mockServerClient;

    // tests
}
  • We want to test our fully integrated application, therefore we use @SpringBootTest to start the complete application context.
  • The @MockServerTest annotation marks the test class for the TestExecutionListener. We also overwrite the swapi.baseUrl property used by our StarWarsCharacterRepository. This way all its requests will be targeted against the MockServer. The ${mockServerPort} placeholder will be replaced with the randomly assigned port of the MockServer.
  • The TestExecutionListener searches for fields of the type MockServerClient and assigns an instance to them.
    We will use this in our tests to set expectations.

With our prepared test class, we can now start to implement the test:

CharacterControllerIT.java
@Test
void requestingAKnownCharacter_Returns200() throws Exception {

        final var resource = new ClassPathResource("luke.json");
        final var mockedResponse = Files.readString(Path.of(resource.getURI()));

        mockServerClient
                .when(request().withPath("/people/1"))
                .respond(response()
                                 .withStatusCode(HttpStatusCode.OK_200.code())
                                 .withContentType(org.mockserver.model.MediaType.APPLICATION_JSON)
                                 .withBody(mockedResponse));

        mockMvc.perform(get("/characters/1"))
               .andExpect(status().isOk())
               .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE))
               .andExpect(jsonPath("$.name", equalTo("Luke Skywalker")));
    }
  • We load luke.json from the test resources. The content will be used as a mocked response by MockServer.
  • We use the MockServerClient to set an expectation. Every time someone does a request against “/people/1” MockServer will respond with the content of the luke.json, the status code 200 OK, and the HTTP header Content-Type with the value application/json.
  • We use MockMvc to make a request against our CharacterController and some assertions about the response.

Conclusion

As you could see in the example, MockServer enables us to test our fully integrated application. In combination with @RestClientTests, this gives us a well-rounded test suite for our HTTP clients. The fact that it comes in one package containing all dependencies makes it easy to add to your project. With MockServerClient’s easy-to-learn API, you can start writing your expectations in no time. The TestExecutionListener integrates well into Spring Boot and allows you to configure your application dynamically. On top of that, it will also reset the MockServer after each test. This way the expectations set in one test won’t be carried over to the next, which means each one runs in isolation.

Resources