Skip to Content

A WireMock tutorial

Posted on 11 mins read
Tags: testing, java

Foreword #

Recently at work we had to work with legacy Java code that among other things needs to make HTTP calls to an external API.

The tests for the HTTP client were written with WireMock, but we had an hard time debugging the tests failures.

While working together on those tests we discovered a better way to use WireMock, and this is how this tutorial was born.

Note that you can find the full code and its history on my forge.

Enjoy!

Part 1 : The External API #

So, for this tutorial to make sense we are going to need an API to call.

Let’s pretend we are using an API provided by a public library.

Among the various routes we can call, there is one to get some statistics about the library:

// GET /library/stats

{
  "book_count": 2000
}

And we also have a route to checkout a book using its ISBN:

// POST /books/checkout

{
  "isbn": "1234556"
}

If the book is available, we will get this answer (with a 200 OK status)

{
  "status": "booked"
}

Part 2 : first try using WireMock #

First iteration of the Java client #

Let’s define some classes to represent the various responses and requests:

class StatsResponse {
    public int book_count;
}
record CheckoutRequest(String isbn) {

}
class CheckoutResponse {
    public String status;
}

Then we can write a class named LibraryClient with two methods: getBookCount and checkoutBook:

public class LibraryClient {
    private String baseUrl;

    public LibraryClient(String baseUrl) {
        this.baseUrl = baseUrl;
    }

    public int getBookCount() throws Exception {
        var httpClient = HttpClient.newBuilder().build();

        var uri = new URI(baseUrl + "/library/stats");

        var request = HttpRequest.newBuilder()
                .uri(uri)
                .header("Accept", "application/json")
                .GET()
                .build();

        var bodyHandler = HttpResponse.BodyHandlers.ofString();
        var response = httpClient.send(request, bodyHandler);
        var body = response.body();

        var objectMapper = new ObjectMapper();
        var stats = objectMapper.readValue(body, StatsResponse.class);
        return stats.book_count;
    }

    public void checkoutBook(String isbn) throws Exception {
        var httpClient = HttpClient.newBuilder().build();
        var objectMapper = new ObjectMapper();

        var uri = new URI(baseUrl + "/books/checkout");
        var checkoutRequest = new CheckoutRequest(isbn);
        var requestJson = objectMapper.writeValueAsString(checkoutRequest);
        var requestBody = HttpRequest.BodyPublishers.ofString(requestJson);

        var request = HttpRequest.newBuilder()
                .uri(uri)
                .header("Accept", "application/json")
                .header("Content-Type", "application/json")
                .POST(requestBody)
                .build();

        var responseHandler = HttpResponse.BodyHandlers.ofString();
        var response = httpClient.send(request, responseHandler);
        var statusCode = response.statusCode();
        if (statusCode != 200) {
            throw new Exception("Request failed with status: " + statusCode);
        }

        var responseJson = response.body();
        var checkoutResponse = objectMapper.readValue(responseJson,
                CheckoutResponse.class);

        var checkoutStatus = checkoutResponse.status;

        if (!checkoutStatus.equals("booked")) {
            throw new Exception("Invalid status: " + checkoutStatus);
        }
    }
}

Nothing fancy here, we’re just using the HttpClient provided by the java.net package.

To make sure everything works as expected, we can write a simple main:

public class Application {
    public static void main(String[] args) throws Exception {
        var client = new LibraryClient("http://localhost:5000");

        var bookCount = client.getBookCount();

        System.out.println("There are " + bookCount + " books in the library");

        var isbn = "123456";

        System.out.println("Trying to checkout a book ...");

        client.checkoutBook(isbn);

        System.out.println("OK!");
    }
}

An run it:

$ ./mvnw -q compile exec:java
There are 42 books in the library
Trying to checkout a book ...
OK!

First test #

Now let’s add a test for the getBookCount method:

@WireMockTest
public class LibraryClientTest {
    @Test
    void get_books_count(WireMockRuntimeInfo wireMockRuntimeInfo) throws Exception {
        var baseUrl = wireMockRuntimeInfo.getHttpBaseUrl();
        var client = new LibraryClient(baseUrl);

        String mockedResponse = """
                { "book_count": 42 }
                """;
        stubFor(get("/library/stats").willReturn(ok(mockedResponse)));

        var actual = client.getBookCount();
        assertThat(actual).isEqualTo(42);
    }
}

This test passes, but we don’t write tests so that they pass - we write them so that they tell us if something is wrong in the production code :)

So let’s introduce a bug and see if the test fails and how.

First error #

To show an example of failure, let’s replace the path in the production code :

     public int getBookCount() throws Exception {
         var httpClient = HttpClient.newBuilder().build();
 
-        var uri = new URI(baseUrl + "/library/stats");
+        var uri = new URI(baseUrl + "/stats");
    }

Here’s the error message we get:

[ERROR] LibraryClientTest.get_books_count(WireMockRuntimeInfo)
com.fasterxml.jackson.core.JsonParseException: 
Unrecognized token 'Request': was expecting 
  (JSON String, Number, Array, Object or token 'null', 'true' or 'false')

Hum. Not very clear.

Let’s print the body:


            Request was not matched
            =======================

--------------------------------------------------------------
| Closest stub        | Request                               |
---------------------------------------------------------------
                      |
GET                   | GET
/library/stats        | /stats     <<<<< URL does not match
                      |
---------------------------------------------------------------

By digging a bit in the WireMock documentation, we discover that when the WireMock server gets a request it cannot handle, rather than throwing an exception, it returns a 404 error message with a body in plain text showing the “closest stub” and the actual request shown side by side.

And since this plain text is not a valid JSON document, a JsonParseException is raised.

Let’s keep going.

Second test #

The test for the checkoutBook method is similar, but instead of stubFor(get(...)) we can use stubFor(post()) with the expected request and the mocked response:

@WireMockTest
public class LibraryClientTest {
    @Test
    void checkout_a_book(WireMockRuntimeInfo wireMockRuntimeInfo) throws Exception {
        var baseUrl = wireMockRuntimeInfo.getHttpBaseUrl();
        var client = new LibraryClient(baseUrl);

        var expectedRequest = """
                  {"isbn": "123456"}
                """;

        var mockedResponse = """
                  { "status": "booked" }
                """;

        stubFor(
                post("/books/checkout")
                        .withRequestBody(equalToJson(expectedRequest))
                        .willReturn(ok(mockedResponse)));

        client.checkoutBook("123456");
    }
}

Note there is no explicit assertion here, but the test will fail if checkoutBook raises an exception, which is what we want.

Second error #

This time let’s make a breaking change in the production code by replacing isbn with id when calling the /books/checkout route.

We get an 404 status code with the following body:

------------------------------------------------------------------------------
                        |
POST                    | POST
/books/checkout         | /books/checkout
                        |
[equalToJson]           |                          <<<<< Body does not match
{                       | {
  "isbn" : "123456"     |   "id" : "123456"
}                       | }
                        |
------------------------------------------------------------------------------

This does not seems right …

It looks like what we are looking at are logs meant for human to read, and not familiar test failures (in the form “expected <…> but was <…>”)

Part 3 : Taking a step back #

Let’s think about what our tests are doing here.

First, when we refactor our implement new features, they need to tell us if we are calling the API from the other team correctly,

Second, any time the external API changes, we’ll update both the production and test code to reflect those changes and check if they still pass.

That’s why we can call those contract tests - the check if the API contract between our code an the external API is met.

And it turns out WireMock is a library that can do a lot of things, but in the context of contract tests we are actually using its API incorrectly!

In fact, we need to do things in two steps:

  1. Configure WireMock server so that it knows what to return when a route is called
  2. Verify that the actual requests that are made by the production code match an expected value.

Turns out the stubFor method is only supposed to be used for the first part !

For the second part, you actually need to call a method named verify

Thinking about the first test again #

Since we are making a ‘GET’ request, there’s no request body to check, but we still should check the URL and the headers.

This can be done with a call to verify like this:

String mockedResponse = """
        { "book_count": 42 }
        """;
stubFor(get("/library/stats").willReturn(ok(mockedResponse)));

var actual = client.getBookCount();
assertThat(actual).isEqualTo(42);

verify(
        getRequestedFor(urlEqualTo("/library/stats"))
                .withHeader("Accept", equalTo("application/json")));

Note that we have to hard-code the path /library/stats twice which is a big annoying - but we’ll deal with that later.

This test passes, and just like before, we can introduce a regression in the production code:

  var request = HttpRequest.newBuilder()
          .uri(uri)
-          .header("Accept", "application/json")
+          // .header("Accept", "application/json")
          .GET()
          .build();

And see the failure:

No requests exactly matched. 
Most similar request was:  expected:<
GET
/library/stats

Accept: application/json
> but was:<
GET
/library/stats


>

Rewriting the second test #

Thinking about the second test again, it’s now clear that the expectedRequest string must be removed from the stubFor call - and be passed to a verify call instead.

  stubFor(
      post("/books/checkout")
-            .withRequestBody(equalToJson(expectedRequest))
             .willReturn(ok(mockedResponse)));

+ verify(
+    postRequestedFor(urlEqualTo("/books/checkout"))
+            .withRequestBody(equalToJson(expectedRequest)));

And by renaming ‘isbn’ to ‘id’ in the production code, we do get the error message we were expecting :

No requests exactly matched. Most similar request was:  expected:<
POST
/books/checkout

Accept: application/json

[equalToJson]
{
  "isbn" : "123456"
}> but was:<
POST
/books/checkout

Accept: application/json


{
  "id" : "123456"
}>

By the way, if you run this test in IntelliJ, you should even get a link named ‘click to see the difference’, showing you the expected and actual strings side by side, which is great when the expected JSON string spans lots of lines.

Part 4 : Reducing duplication in tests #

OK, now that we have a test that behaves the way we want, it’s time to reduce duplication a bit :)

We can start by extracting a getClient method in the tests:

private LibraryClient getClient(WireMockRuntimeInfo wireMockRuntimeInfo) {
    var baseUrl = wireMockRuntimeInfo.getHttpBaseUrl();
    return new LibraryClient(baseUrl);
}

And then a mockResponse method:

private void mockResponse(RequestMethod method, String path, String mockedResponse) {
    MappingBuilder mappingBuilder = null;
    if (method.equals(RequestMethod.GET)) {
        mappingBuilder = get(path);
    } else {
        mappingBuilder = post(path);
    }

    stubFor(mappingBuilder.willReturn(ok(mockedResponse)));
}

Our first test now looks like this:

@Test
void get_books_count(WireMockRuntimeInfo wireMockRuntimeInfo) throws Exception {
    var client = getClient(wireMockRuntimeInfo);
    mockResponse(GET, "/books/count", """
                { "count": 42 }
            """);

    var actual = client.getBookCount();

    assertThat(actual).isEqualTo(42);
}

Now we can do the same for the second test:

@Test
void checkout_a_book(WireMockRuntimeInfo wireMockRuntimeInfo) throws Exception {
  var client = getClient(wireMockRuntimeInfo);

  mockResponse(POST, "/books/checkout", """
                        { "status": "booked" }
            """);


  client.checkoutBook("1234546");

  var expectedRequest = """
            {"isbn": "123456"}
          """;

  verify(
      postRequestedFor(urlEqualTo("/books/checkout"))
              .withRequestBody(equalToJson(expectedRequest)));

}

Part 5: Keep reducing duplication in tests #

OK - time to deal with the repeated hard-coded paths.

We’ll need to store them as fields in a class that will call stubFor and verify.

While we’re at it, we’ll split the “GET” and “POST” into separate classes.

Here’s what it looks like :

public class GetChecker {
    private String path;

    public GetChecker(String path) {
        this.path = path;
    }

    public void willReturn(String mockedResponse) {
        stubFor(WireMock.get(path).willReturn(ok(mockedResponse)));
    }
}
public class PostChecker {
    private String path;

    public PostChecker(String path) {
        this.path = path;
    }

    public void willReturn(String mockedResponse) {
        stubFor(WireMock.post(path).willReturn(ok(mockedResponse)));
    }

    public void assertRequestedWith(String expectedRequest) {
        postRequestedFor(
                urlEqualTo(path))
                .withRequestBody(equalToJson(expectedRequest));
    }
}

Now we can introduce a few helper methods in the test class


@WireMockTest
public class LibraryClientTest {
    private PostChecker postChecker;
    private GetChecker getChecker;

    private PostChecker post(String path) {
        postChecker = new PostChecker(path);
        return postChecker;
    }

    private GetChecker get(String path) {
        getChecker = new GetChecker(path);
        return getChecker;
    }

    private void assertRequestedWith(String string) {
        postChecker.assertRequestedWith(string);
    }
}

And rewrite the two tests:

    @Test
    void get_books_count(WireMockRuntimeInfo wireMockRuntimeInfo) throws Exception {
        var client = getClient(wireMockRuntimeInfo);

        get("/library/stats").willReturn("""
                    { "book_count": 42 }
                """);

        var actual = client.getBookCount();

        assertThat(actual).isEqualTo(42);
    }


    @Test
    void checkout_a_book(WireMockRuntimeInfo wireMockRuntimeInfo) throws Exception {
        var client = getClient(wireMockRuntimeInfo);

        post("/books/checkout").willReturn("""
                        { "status": "booked" }
                """);

        client.checkoutBook("123456");

        assertRequestedWith("""
                        {"isbn": "123456"},
                """);
    }

Conclusion #

And there you have it.

First, we understood how to properly use the WireMock API for contract tests (by using both the stubFor and the verify) calls.

And then we used the ’extract method’ and ’extract class’ refactorings a few times in order to encode our gained knowledge inside the test code.

This is a general technique you can use any time you hare using a complex API and want to simplify its usage for your fellow devs. (Look out the “Facade” design pattern to know more).

Bonus: refactoring the production code #

Now that we have a nice suite of tests, why not take some time to refactor the production code too?

First, initialize the client and the object mapper once, in the constructor:

public class LibraryClient {
    private String baseUrl;
    private HttpClient httpClient;
    private ObjectMapper objectMapper;

    public LibraryClient(String baseUrl) {
        this.baseUrl = baseUrl;
        httpClient = HttpClient.newBuilder().build();
        objectMapper = new ObjectMapper();
    }
}

Then, introduce a sendRequest() method:

public class LibraryClient {
    private String sendRequest(HttpRequest request) throws Exception {
        var response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
        var body = response.body();

        var statusCode = response.statusCode();
        var method = request.method();
        var path = request.uri().getPath();
        if (statusCode != 200) {
            throw new ClientError(method, path, statusCode, body);
        }

        return body;
    }
}

Note the usage of a custom ClientError exception, so that we always have access to the body if the request fails, which will greatly help debugging problems in the future.

public class ClientError extends Exception {
    private String method;
    private String path;
    private int statusCode;
    private String body;

    public ClientError(String method, String path, int statusCode, String body) {
        this.method = method;
        this.path = path;
        this.statusCode = statusCode;
        this.body = body;
    }

    @Override
    public String toString() {
        String message = method + " " + path + " failed with status: " + statusCode;
        message += "\n" + body;
        return message;
    }
}

Now we can extract getJson and postJson:

public class LibraryClient {
    // ....

    private <T> T getJson(
            String path, Class<T> valueType) throws Exception {
        var uri = new URI(baseUrl + path);
        var request = HttpRequest.newBuilder()
                .uri(uri)
                .header("Accept", "application/json")
                .GET()
                .build();

        var json = sendRequest(request);
        return objectMapper.readValue(json, valueType);
    }

    private String postJson(String path, Object value) throws Exception {
        var json = objectMapper.writeValueAsString(value);
        var uri = new URI(baseUrl + path);
        var requestBody = HttpRequest.BodyPublishers.ofString(json);

        var request = HttpRequest.newBuilder()
                .uri(uri)
                .header("Accept", "application/json")
                .header("Content-Type", "application/json")
                .POST(requestBody)
                .build();

        return sendRequest(request);
    }
}

And finally :

public class LibraryClient {
    // ... 

    int getBookCount() throws Exception {
        var stats = getJson("/library/stats", StatsResponse.class);
        return stats.book_count;
    }

    public void checkout(CheckoutRequest request) throws Exception {
        postJson("/books/checkout", new CheckoutRequest(isbn));
    }
}

I hope you’ll agree it’s a much better code than the one we had at the beginning of the tutorial :)


Thanks for reading this far :)

I'd love to hear what you have to say, so please feel free to leave a comment below, or read the contact page for more ways to get in touch with me.

Note that to get notified when new articles are published, you can either:

Cheers!