Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Using Wiremock to Test Against 3rd Party Services Offline

DZone's Guide to

Using Wiremock to Test Against 3rd Party Services Offline

Tools like Wiremock can allow you to finish your API implementation on a plane with no internet.

· Java Zone
Free Resource

Learn how to troubleshoot and diagnose some of the most common performance issues in Java today. Brought to you in partnership with AppDynamics.

I’ve recently been playing Stockfighter, the programming game released by Patrick McKenzie. It’s great fun and you should totally try it. The game is based mostly around a REST API, and so I ended up needing to create a basic Java abstraction of the API.

Being a bit obsessed with TDD I wanted to create some tests for this first. Writing code to call third party APIs and testing by calling the external service can be immensely frustrating and slow, so I wanted a better way to stub out standard responses. I’m also currently travelling a lot, often without internet, but wanted to be able to keep coding up against the API having saved the docs offline (thanks evernote!)

There are a number of libraries available for this, but none of them struck me as a good way of doing TDD until I stumbled across Wiremock. As you can imagine from the name, it can mock third party calls and allow you to make assertions on them. It uses a Hamcrest like syntax, and also allows you to match things like the request body arguments. I’m a huge fan, and it allowed me to complete my API implementation on a plane with no internet.

An Example

Imagine a stock exchange where you submit an order using a POST request. Don’t worry if you don’t know finance, just know that you’re submitting some JSON and you’re getting a JSON response back.

I’m writing code test first, so this is exactly what I want to test for: I expect a POST request to a URL and if I receive it, I want to send a stub response back.

String url = “/ob/api/venues/MFSEX/stocks/BYSE/orders”

stubFor(post(urlPathMatching(url))

Fairly self explanatory so far. Wiremock uses a wonderful syntax based on static imports; stubFor, post and urlPathMatching are all from the library.

So we’ve specified the path we want to match. If this matches, we need to tell it how to respond.

 stubFor(post(urlPathMatching(url))
    .willReturn(aResponse()
                        .withStatus(200)
                        .withHeader("Content-Type", "application/json")
                        .withBody("{\n" +
                                "    \"ok\": true,\n" +
                                "    \"symbol\": \"BYSE\",\n" +
                                "    \"venue\": \"MFSEX\",\n" +
                                "    \"direction\": \"buy\",\n" +
                                "    \"originalQty\": " + filledTradeCount + ",\n" +
                                "    \"qty\": 0,\n" +
                                "    \"price\": 0,\n" +
                                "    \"orderType\": \"" + orderType + "\",\n" +
                                "    \"id\": 1051,\n" +
                                "    \"account\": \"WPP20023868\",\n" +
                                "    \"ts\": \"2015-12-16T04:18:46.42516234Z\",\n" +
                                "    \"fills\": [\n" +
                                "        {\n" +
                                "            \"price\": 3466,\n" +
                                "            \"qty\": " + filledTradeCount + ",\n" +
                                "            \"ts\": \"2015-12-16T04:18:46.425164757Z\"\n" +
                                "        }\n" +
                                "    ],\n" +
                                "    \"totalFilled\": " + filledTradeCount + ",\n" +
                                "    \"open\": false\n" +
                                "}")));

All of the response fields are optional. From my JSON response I’ve extracted out some variables as I want to check that my API parses the response appropriately.

All I need to get the Wiremock server going is a ClassRule:

@ClassRule
public static WireMockRule wireMockRule = new WireMockRule(8089);

This starts wiremock on port 8089 locally. In my test, I just tell it the server to point at is localhost:8089 and it will make the third party API calls there, matching up with the wiremock calls we’ve stubbed out.

A Further Example

Often you won’t just want to match against the URL being called, but also the content. Fortunately this is easy with Wiremock:

stubFor(post(urlPathMatching(url))
                .withRequestBody(equalToJson(
                        "{" +
                                "\"orderType\": \"" + orderType + "\"," +
                                "\"price\": " + price + "," +
                                "\"qty\": " + filledTradeCount + "," +
                                "\"account\": \"" + account + "\"," +
                                "\"direction\": \"buy\"" +
                                "}", JSONCompareMode.LENIENT))
                .willReturn(aResponse()
                        .withStatus(200)….

As you can imagine, there are a host of matching methods available such as equalTo(), equalToXml(), containing(), notMatching() and matchingJsonPath(). It’s a very expressive library.

Putting it all together:

import static com.github.tomakehurst.wiremock.client.WireMock.*;
import org.junit.ClassRule;
import org.junit.Test;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.fail;

public class StockfighterAPITest {
    @ClassRule
    public static WireMockRule wireMockRule = new WireMockRule(8089);
    private String testServer = "http://localhost:8089";

    @Test
    public void basicTradeExecutes() throws Exception {
        int filledTradeCount = 104;
        int price = 12311;
        String account = "WPP20023868";

        String orderType = "market";
        String url = "/ob/api/venues/MFSEX/stocks/BYSE/orders";

        stubFor(post(urlPathMatching(url))
                .withRequestBody(equalToJson(
                        "{" +
                                "\"orderType\": \"" + orderType + "\"," +
                                "\"price\": " + price + "," +
                                "\"qty\": " + filledTradeCount + "," +
                                "\"account\": \"" + account + "\"," +
                                "\"direction\": \"buy\"" +
                                "}", JSONCompareMode.LENIENT))
                .willReturn(aResponse()
                        .withStatus(200)
                        .withHeader("Content-Type", "application/json")
                        .withBody("{\n" +
                                "    \"ok\": true,\n" +
                                "    \"symbol\": \"BYSE\",\n" +
                                "    \"venue\": \"MFSEX\",\n" +
                                "    \"direction\": \"buy\",\n" +
                                "    \"originalQty\": " + filledTradeCount + ",\n" +
                                "    \"qty\": 0,\n" +
                                "    \"price\": 0,\n" +
                                "    \"orderType\": \"" + orderType + "\",\n" +
                                "    \"id\": 1051,\n" +
                                "    \"account\": \"WPP20023868\",\n" +
                                "    \"ts\": \"2015-12-16T04:18:46.42516234Z\",\n" +
                                "    \"fills\": [\n" +
                                "        {\n" +
                                "            \"price\": 3466,\n" +
                                "            \"qty\": " + filledTradeCount + ",\n" +
                                "            \"ts\": \"2015-12-16T04:18:46.425164757Z\"\n" +
                                "        }\n" +
                                "    ],\n" +
                                "    \"totalFilled\": " + filledTradeCount + ",\n" +
                                "    \"open\": false\n" +
                                "}")));

        Trade trade = new StockfighterAPI(testServer, "MFSEX").trade(104, price, account, "BYSE", Trade.LIMIT);

        assertThat(trade.fills.size(), is(1));
        assertThat(trade.fills.get(0).qty, is(filledTradeCount));
        assertThat(trade.orderType, is(orderType));

    }

Testing Error Handling

I have found that REST APIs lend themselves nicely to this sort of testing. As parts of the URL change based on the requst (e.g. the URL varies if I make a request for stock ABC compared to stock XYZ) it makes it very easy to map different URLs to different responses.

  String url = "/ob/api/venues/TESTEX2/stocks/FOOBAR/orders/";
        stubFor(post(urlPathMatching(url))
                .willReturn(aResponse()
                        .withStatus(401)
                        .withHeader("Content-Type", "application/json")
                        .withBody("{" +
                                "  \"ok\": false," +
                                "  \"error\": \"" + errorMessage + ".\"" +
                                "}")));

This is nicely segregated from my earlier post call.

Room For Improvement

At the moment the library follows the mockito style where it doesn’t blow up if there are unexpected calls made; this can be quite frustrating as a number of times I’d get an error in my parsing code, only to discover that Wiremock had failed to catch my request and had returned the standard 404 page. Whilst the error was on my part, I would have liked Wiremock to tell me that an unexpected call was made. If you’re used to Mockito perhaps this won’t be such an issue, but from a JMock perspective it was a different way of working. However the project owner was quick to respond on the mailing list which I was really impressed by. This is only a minor gripe and it’s definitely a library I’ll be using on my projects going forward.

The examples here are just a small fraction of what Wiremock can do; it can also proxy through to the real service, record responses from the actual API, and simulate difficult errors like response delays and malformed responses. Hopefully I’ve given you a taste for what’s possible and you’ll try it in your apps.

You can see the full test and source code on GitHub at: https://github.com/samberic/stockfighter

Follow me on Twitter via @SambaHK

Understand the needs and benefits around implementing the right monitoring solution for a growing containerized market. Brought to you in partnership with AppDynamics.

Topics:
java ,tdd ,rest ,wiremock

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}