Over a million developers have joined DZone.

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

Navigate the Maze of the End-User Experience and pick up this APM Essential guide, brought to you in partnership with CA Technologies

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

Thrive in the application economy with an APM model that is strategic. Be E.P.I.C. with CA APM.  Brought to you in partnership with CA Technologies.

Topics:
java ,tdd ,rest ,wiremock

Opinions expressed by DZone contributors are their own.

The best of DZone straight to your inbox.

SEE AN EXAMPLE
Please provide a valid email address.

Thanks for subscribing!

Awesome! Check your inbox to verify your email so you can start receiving the latest in tech news and resources.
Subscribe

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

{{ parent.tldr }}

{{ parent.urlSource.name }}