Mocking a gRPC service
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
| To | Step |
|---|---|
| answer | stub "users.v1.UserService/GetUser" returns json: |
| answer only some calls | … with request json "id" "42" returns …, … with metadata "authorization" "Bearer x" returns … |
| fail | returns status "NOT_FOUND" with message "…" (any gRPC status) |
| be slow, or vary | returns in sequence: a table of json, status, message, delay |
| stream | streams: one message per row, for server-streaming methods |
| be down | refuses connections, then accepts connections |
| check what arrived | received "…/GetUser" "2" times, last "…/GetUser" request json contains: |
Notes
- The most specific stub wins, then the latest, so a scenario can override
what its
Backgroundstubbed. - A delay respects the caller’s deadline: give the call one second and it
gets
DEADLINE_EXCEEDEDafter one second. - After
refuses connections, a gRPC client backs off before it reconnects. A service that retriesUNAVAILABLEwith 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.