← Mocking third-party dependencies

Mocking a gRPC service

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

What it needs: the schema

A gRPC stub has to know the messages to answer with. Give it your .proto files compiled to a descriptor set:

buf build -o proto/users.binpb
# or
protoc --include_imports --descriptor_set_out=proto/users.binpb -I proto proto/users/v1/users.proto

With that, you write stubs and assertions in JSON, and a typo in a method or field fails at the step that has it.

The suite

app:
  command: node app.js
  env:
    USERS_ADDR: "{{.users-stub.address}}"   # localhost:9102

resources:
  shop-client:   { type: http, base_url: http://localhost:3000 }
  users-stub: { type: grpc-server, options: { port: 9102, descriptor_set: proto/users.binpb } }
Scenario: A known user
  Given "users-stub" stub "users.v1.UserService/GetUser" with request json "id" "42" returns json:
    """
    {"id": "42", "name": "Ada", "plan": "pro"}
    """
  When "shop-client" sends "GET" to "/users/42"
  Then "shop-client" response json "name" is "Ada"
  And "users-stub" last "users.v1.UserService/GetUser" request metadata "authorization" is "Bearer secret"

Scenario: An unknown user
  Given "users-stub" stub "users.v1.UserService/GetUser" returns status "NOT_FOUND" with message "no user 7"
  When "shop-client" sends "GET" to "/users/7"
  Then "shop-client" response status is "404"

What it can do

ToStep
answerstub "users.v1.UserService/GetUser" returns json:
answer only some calls… with request json "id" "42" returns …, … with metadata "authorization" "Bearer x" returns …
failreturns status "NOT_FOUND" with message "…" (any gRPC status)
be slow, or varyreturns in sequence: a table of json, status, message, delay
streamstreams: one message per row, for server-streaming methods
be downrefuses connections, then accepts connections
check what arrivedreceived "…/GetUser" "2" times, last "…/GetUser" request json contains:

Notes

  • The most specific stub wins, then the latest, so a scenario can override what its Background stubbed.
  • A delay respects the caller’s deadline: give the call one second and it gets DEADLINE_EXCEEDED after one second.
  • After refuses connections, a gRPC client backs off before it reconnects. A service that retries UNAVAILABLE with a short backoff recovers at once; one with the library defaults may not, which is worth a scenario of its own.
  • Unary and server-streaming only for now; client and bidirectional streams can’t be stubbed yet.