← Mocking third-party dependencies

Mocking a WebSocket feed

stubswebsocket
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.

The suite

The service here connects to a price feed when it starts, sends subscribe BTC, and keeps the latest price.

app:
  command: node app.js
  env:
    PRICES_URL: ws://localhost:9103/stream   # any path is accepted

resources:
  shop-client:    { type: http, base_url: http://localhost:3000 }
  prices-stub: { type: websocket-server, options: { port: 9103 } }
Background:
  Given "prices-stub" on message "subscribe BTC" replies:
    """
    {"symbol": "BTC", "price": 64000}
    """

Scenario: The service subscribes when it connects
  Then "prices-stub" has "1" connections
  And "prices-stub" received message "subscribe BTC"

Scenario: The service keeps the latest price the feed pushes
  Given "prices-stub" has "1" connections
  When "prices-stub" broadcasts:
    """
    {"symbol": "BTC", "price": 65000}
    """
  And "shop-client" sends "GET" to "/prices/BTC"
  Then "shop-client" response json "price" is "65000"

What it can do

ToStep
greet a new connectionon connect sends:
reply to a messageon message "subscribe BTC" replies:, or on message matching "subscribe .*" replies:
push to everyone connectedbroadcasts:
check the connectionhas "1" connections
check what arrivedreceived message "subscribe BTC", received "3" messages

The one thing to know: it hangs up

Before every scenario the stub closes every open connection, as well as forgetting its replies and messages. A service that keeps one long-lived connection must reconnect, and it does so a moment after the scenario starts. So:

  • Wait for the connection before pushing. Given "prices-stub" has "1" connections waits until the service is back. Broadcasting before that fails with “no clients are connected”.
  • That’s a feature. Real feeds drop connections too. Every scenario starts with your service reconnecting and resubscribing, so a service that can’t is caught on the first scenario, not in production.

Notes

  • No address template yet: name the port in options.port and write the same address in app.env.
  • Messages match exactly, or by regular expression with matching.
  • Text and JSON both work; JSON is compared as text, so match the exact bytes your service sends.