Integration Testing Spring Boot based Microservices using Spring REST Docs and WireMock

Steve Kiley
Jan 20 · 6 min read
WireMock logo next to Spring Boot logo
WireMock logo next to Spring Boot logo

There is no free lunch when it comes to embracing microservices, a wise person once said. While there are many advantages to the approach, it comes with some undeniable challenges. One of these challenges relates to achieving proper test coverage across your system. In this blog, I will explain how I was able to tackle this, in integration-testing my Spring Boot based REST API microservices using Spring REST Docs and WireMock.

Why Use WireMock?

In mocking microservices, I aim to keep their respective integration tests as lightweight as possible. Since it’s advisable to test in isolation, simply connecting to a staging instance of this service won’t cut it. If we test against a dataset that can change unpredictably, these tests are susceptible to unpredictable, confusing, and misleading failures.

Can we spin up a “real” system instance which remains isolated per-test? We could use a tool like Docker Compose to spin up a handful of containers which make up our stack of microservices, along with components like Redis, Kafka, MySQL, etc. But, in ensuring complete isolation between tests, this whole stack would have to be destroyed and rebuilt after each test execution. Resource requirements aside, this would make for an incredibly slow CI build. That is why I decided to embrace mocks.

How do I know my mocks will reflect actual system behavior?

We need a tool which verifies that our REST API endpoints are behaving as expected. That same tool should produce a mock REST API which mimics the behavior we just verified to be working. In this case, the tool I have wielded is a combination of these libraries:

  • Spring Boot
  • Spring MVC
  • Spring REST Docs
  • WireMock

Tutorial

First, let’s add a few things to your Gradle script:

plugins {
id 'maven-publish'
id 'org.asciidoctor.convert' version '1.5.9.2'
}

Next, we need to modify our existing Spring MockMvc tests so that WireMock stubs are generated upon successful execution. Let’s add a few auto-configuration annotations to each test class:

@AutoConfigureWireMock
@AutoConfigureRestDocs

This will only work if MockMvc itself has been auto-configured.

At this point, your MockMvc tests should look something like this:

mockMvc.perform(post(requestUrl)
.contentType(MediaType.APPLICATION_JSON)
.content(requestPayloadString))
.andExpect(status().isCreated())

Here, you are invoking a REST API call and ensuring that the proper response code is returned. Upon successful test execution, we want to expose the endpoint in our mock REST API for other projects to integration-test. You generally don’t want to associate a mock response with this exact request, byte-by-byte. Instead, you can add a few more lines of code to specify when the mock response should be returned, and verify that the original MockMvc request would match these conditions.

.andDo(verify()
.wiremock(WireMock.post(urlMatching("\\/media\\/[a-f\\d]{24}\\/taskSubmissions"))
.withRequestBody(matchingJsonPath("$.annotationIds", matching("^\\[.*\\]$")))
.withRequestBody(matchingJsonPath("$.submittedAt"))

We are checking a few conditions here:

  1. The request URL matches the provided regex pattern.
  2. The value for JSON field “annotationIds”, as part of the request payload, matches the provided regex pattern.
  3. The JSON field “submittedAt”, simply, is present in the request payload.

Lets add one more line of code:

.andDo(document("postTaskSubmissions"))

Through the magic of Spring, this will generate a WireMock stub called postTaskSubmissions.json, and an AsciiDoc which documents a example of the request. Documentation is critical; this will empower the rest of your team to use these generated mocks with ease.

You can define a Maven publication in your Gradle script; that will allow you to upload a mock JAR to a repository via gradle publish. I recommend using Gitlab for this. This will allow others to utilize these stubs without checking out the respective git repository. You can also run gradle publishToMavenLocal, in lieu of a remote repository configuration, for use in local integration testing.

You must run all of your MockMvc based tests to generate these stubs before attempting to publish the JAR.

You can run gradle asciidoctor to convert the aforementioned AsciiDoc into HTML. Here’s an example:

Spring REST Docs html screengrab
Spring REST Docs html screengrab

Next, let’s set up another Spring Boot project to utilize these mocks!

In your Gradle script, add a WireMock dependency, along with a dependency for the newly generated stubs. If your mock JAR is hosted on a remote repository, make sure you configure your repositories so that Gradle can find the jar.

dependencies { 
testImplementation 'org.springframework.cloud:spring-cloud-contract-wiremock'
testImplementation '[group]:[artifact]-stubs:[version]'
}

Go to your integration test and add a class-level annotation:

@AutoConfigureWireMock(port = [an available port],
stubs = "classpath*:**/[group]/[artifact]/**/mappings/**/*.json")

Now, when we test, we want to invoke this mock REST API, not an actual instance. For example, instead of invoking a REST API hosted at http://api.medium.com, point to http://localhost:[an available port]. You can use the concept of profiles in Spring Boot to easily switch between base URLs per-environment.

That’s really it! In your integration test, when you make an HTTP request to this endpoint, WireMock will attempt to match the request. Remember, the request will have to match what we specified in the service’s MockMvc test, per method call .andDo(verify().... When a request is successfully matched, the response that’s returned is equivalent to what was returned while executing the respective MockMvc test. This exact response (JSON payload, status code, and headers) is detailed in the auto-generated AsciiDoc. If WireMock is unable to match the request, a 404 code will be returned. The response payload will provide additional information:

Image for post
Image for post

If your microservice ecosystem is purely Spring Boot based, then I do not have much else to offer; you are ready to integration test! However, if you work in a polyglot environment, you might be wondering how you could integration test in other languages like JavaScript or Python. We can do so with the help of Docker.

Bonus Section: Docker tutorial for polyglot integration testing

We will need to create a Dockerfile which contains a WireMock binary, along with the stubs that are generated after successful text execution:

FROM rodolpheche/wiremock:2.27.2

RUN mkdir -p /home/wiremock/mappings
COPY stubs /home/wiremock/mappings

This Dockerfile should not be placed in a root project directory. We are not trying to package the project itself into a Docker image. Instead, place it in a subdirectory such as mock-image-build/.

Next, we’ll need to copy our stubs into a directory that Docker will be able to find during the build process.

mkdir -p [directory containing Dockerfile]/stubs
cp /path/to/project/build/generated-snippets/stubs/* [directory containing Dockerfile]/stubs

These commands are to be executed in a script or shell terminal, not within the newly created Dockerfile.

Next, let’s build the image, and push it to a registry:

docker build -t [image name]:[image version/tag] [directory containing Dockerfile]
docker push [image name]:[image version/tag]

You can perform these above steps manually, however, I would recommend that you automate this process in a CI/CD pipeline. For that, again, I recommend Gitlab.

Now, you can go ahead and spin up a container:

docker run -it --rm -p 8080:8080 [registry]/[image name]:[image tag]

This will respond to HTTP calls just as we described in the earlier section. But you are no longer constrained to Spring Boot tests; you can invoke this mock API using any HTTP client written in any language.

In using a tool like Testcontainers, you can programmatically spin-up these lightweight containers in all sorts of integration test suites, whether they are written in Python, JavaScript, Go, etc. Note that WireMock is completely stateless; there is no need to reboot the container after each test to ensure isolation.

Happy testing!

gumgum-tech

Thoughts from the GumGum tech team

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store