← Mocking third-party dependencies

Mocking an HTTP API, and how WireMock does it

stubshttpwiremock
Contents
tomato starts a stub in place of the third party, points the service at it, tells the stub what to answer, and checks what the service sent it.tomato runGiven … stub …When … sends …Then … received …features/*.featureshopyour servicesame code, either wayPAYMENTS_URL=https://api.stripe.comStripenot yoursanswersPOST /v1/charges → 201receivedPOST /v1/chargesHTTPHTTPthe scenario sets the answer, then reads what arrived

In production the service calls a dependency you don't run: Stripe. You can't make it fail on demand, and you can't ask it what your service sent.

Point. tomato starts a http-server stub and hands the service its address in the same variable. The service's code doesn't change.

Arrange. A Given step tells the stub what to answer: a success, an error, a delay, a refused connection.

Act. The scenario calls your service; your service calls the stub, which answers as told and remembers the call.

Assert. Then steps check your service's answer and what it sent the stub: the path, headers, body, metadata, messages.

Reset. Before the next scenario the stub forgets its answers and what it received. Scenarios never see each other's stubs.

How WireMock works

WireMock is the best-known HTTP stub. It is a server with two parts:

  • Mappings: “a request like this gets this response”, loaded from JSON files or posted to its admin API.
  • A journal: every request it received, which a test queries afterwards to check what was sent.

tomato’s http-server is the same idea, inside the suite. A mapping becomes a Given step, a journal query becomes a Then step, and tomato clears both before every scenario, which with WireMock you do yourself.

WireMock: mappings/charge.json
{
  "request": { "method": "POST", "url": "/v1/charges" },
  "response": {
    "status": 201,
    "jsonBody": { "id": "ch_1" }
  }
}
tomato: a step
Given "stripe-stub" stub "POST" "/v1/charges" returns "201" with json:
  """
  {"id": "ch_1"}
  """

The suite

app:
  command: node app.js
  env:
    PAYMENTS_URL: "{{.stripe-stub.url}}"   # http://localhost:9101

resources:
  shop-client:      { type: http, base_url: http://localhost:3000 }
  stripe-stub: { type: http-server, options: { port: 9101 } }
Scenario: A successful charge
  Given "stripe-stub" stub "POST" "/v1/charges" returns "201" with json:
    """
    {"id": "ch_1", "status": "succeeded"}
    """
  When "shop-client" sends "POST" to "/checkout" with json:
    """
    {"amount": 1999}
    """
  Then "shop-client" response status is "201"
  And "stripe-stub" received "POST" "/v1/charges" with header "Authorization" "Bearer sk_test_123"
  And "stripe-stub" last "POST" "/v1/charges" request json contains:
    """
    {"amount": 1999, "currency": "eur"}
    """

Scenario: The provider is down
  Given "stripe-stub" refuses connections
  When "shop-client" sends "POST" to "/checkout" with json:
    """
    {"amount": 1999}
    """
  Then "shop-client" response status is "503"

What it can do

ToStep
answerstub "POST" "/v1/charges" returns "201" with json:
answer only some requestsstub "GET" "/users" with query "page" "2" returns "200" (also with header, with body containing)
answer differently each callstub "POST" "/v1/charges" returns in sequence: a table of status, body
be downrefuses connections, then accepts connections
check what arrivedreceived "POST" "/v1/charges" "2" times, last "POST" "/v1/charges" request json contains:
reuse a value the service generatedlast "POST" "/v1/charges" request json "id" saved as "{{charge}}"

Notes

  • Give it a fixed port. {{.stripe-stub.url}} needs one to resolve.
  • A request no stub matches gets a 404 that says so, which usually means a wrong path in the stub or in the service.
  • The service runs as a process (app.command) for this. Services tomato runs in containers can’t reach a stub on the host on Linux; for those, a WireMock container with mapping files is the way, as the end-to-end suite of the coverage demo does.