gRPC server configuration¶
The grpc-server resource stands in for a gRPC service your application
calls, the way http-server stands in for an HTTP one. A
scenario stubs the methods it needs, the app calls them as it would the real
service, and the scenario asserts on what the app sent.
The schema¶
A stub has no server to ask for its schema over reflection, the way the
grpc client does, so it is given one: a FileDescriptorSet,
the compiled form of your .proto files. Either tool writes one:
buf build -o proto/users.binpb # imports included
protoc --include_imports --descriptor_set_out=proto/users.binpb users.proto
Commit it next to the suite, or build it in CI before tomato run. With
protoc, --include_imports matters: a set missing an import fails with the
file it is missing. The well-known types (google/protobuf/timestamp.proto
and friends) are the exception; tomato has those built in.
Knowing the schema is what lets stubs and assertions be written as JSON, the
same protojson the grpc client uses, and what makes a mistake fail at the
step that makes it: a method that is not in the set fails with the ones that
are, and a stubbed response with a misspelt field fails with the message type
it did not fit.
Resource configuration¶
| Option | Default | Description |
|---|---|---|
descriptor_set |
— | Required. Path to a FileDescriptorSet (.binpb, .pb), or a list of them. Relative to where tomato runs |
port |
0 (a free port) |
Port to listen on. Fix it so app.env can name the address |
The server speaks plaintext HTTP/2 (h2c), which is what a gRPC client dialing
with insecure credentials expects. It serves every service in the set, and
gRPC server reflection too, so grpcurl -plaintext localhost:9995 list and
tomato's own grpc resource work against it.
It starts before the app does, so an app that dials or checks its dependencies while it starts finds it listening.
Pointing your app at it¶
app.env names the server through its resource name:
app:
command: go run ./cmd/server
env:
USERS_ADDR: "{{.users.address}}" # localhost:9995
USERS_HOST: "{{.users.host}}" # localhost
USERS_PORT: "{{.users.port}}" # 9995
A gRPC target is host:port, not a URL, so the address is the usual one to
use. With the app in a container, the host is host.docker.internal.
Templates need a fixed port; without one they are left as they are.
Stubbing¶
Scenario: The page shows the user's name
Given "users" stub "users.v1.UserService/GetUser" returns json:
"""
{"id": "42", "name": "Alice", "role": "ROLE_ADMIN"}
"""
When "api" sends "GET" to "/profile/42"
Then "api" response json "name" is "Alice"
The docstring is the response message in protojson: field names in
lowerCamelCase (or as in the .proto), enums by name, Timestamp as an RFC
3339 string.
A failure is a status, with an optional message:
Given "users" stub "users.v1.UserService/GetUser" returns status "NOT_FOUND" with message "no user 42"
Status names follow the gRPC spec (NOT_FOUND, PERMISSION_DENIED,
UNAVAILABLE, ...); grpc-go's NotFound and the number work too.
Which calls a stub answers¶
Between the method and returns, conditions narrow the calls a stub answers:
with metadata "authorization" "Bearer {{token}}": a metadata value, exactly.with request json "id" "42": a field of the request message. The path is the one JSON assertions use (user.address.city,items[0].sku), and the value takes the same@matchers:"@startswith:secret-","@gt:10".
Given "users" stub "users.v1.UserService/GetUser" returns json:
"""
{"id": "1", "name": "Bob"}
"""
And "users" stub "users.v1.UserService/GetUser" with request json "id" "404" returns status "NOT_FOUND"
The most specific stub wins, then the most recent, so a scenario can override
what its Background stubbed. A call no stub answers gets UNIMPLEMENTED, with
a message naming the method and, when stubs for it exist, what they need.
Over several calls, and slowly¶
returns in sequence: answers each call with the next row and repeats the
last one. A row is a message (json) or a failure (status, message), and
delay answers late:
Given "users" stub "users.v1.UserService/GetUser" returns in sequence:
| status | message | json | delay |
| UNAVAILABLE | warming up | | |
| OK | | {"id": "42", "name": "Slow"} | 3s |
| OK | | {"id": "42", "name": "Alice"} | |
A delay respects the caller's deadline: an app that gives the call one second
gets DEADLINE_EXCEEDED after one second, and the stub stops waiting.
Server streaming¶
streams: answers a server-streaming method with one message per row, then
ends the stream. A last row with a status ends it with that status instead:
Given "users" stub "users.v1.UserService/WatchUsers" streams:
| json | delay | status | message |
| {"id": "1", "name": "Alice"} | | | |
| {"id": "2", "name": "Bob"} | 50ms | | |
| | | UNAVAILABLE | connection reset |
returns json: on a streaming method sends a stream of one message.
Asserting on what the app sent¶
Then "users" received "users.v1.UserService/GetUser"
And "users" received "users.v1.UserService/GetUser" "2" times
And "users" did not receive "users.v1.UserService/DeleteUser"
And "users" received "3" calls
And "users" received "users.v1.UserService/GetUser" with metadata "authorization" "Bearer {{token}}"
And "users" last "users.v1.UserService/GetUser" request metadata "authorization" is "Bearer {{token}}"
And "users" last "users.v1.UserService/ListUsers" request json contains:
"""
{"pageSize": 50}
"""
The request is rendered as JSON with its zero values, like the grpc
client's responses, so a field that is 0, "" or false is there to
assert on. A value the app generated can be kept for later steps:
And "users" last "users.v1.UserService/GetUser" request json "id" saved as "{{user_id}}"
And "users" last "users.v1.UserService/GetUser" request metadata "x-request-id" saved as "{{request_id}}"
Metadata keys are case-insensitive, as gRPC sends them in lower case.
The service is down¶
Given "users" refuses connections
When "api" sends "GET" to "/profile/42"
Then "api" response status is "503"
When "users" accepts connections
refuses connections closes the port and the connections the app already
has open, so its calls fail with UNAVAILABLE; calls in flight finish first.
accepts connections opens it again on the same address, with the stubs kept.
A gRPC client backs off before reconnecting, so the app's first call after
that may still fail, depending on its channel settings.
Reset behavior¶
Before every scenario the stubs and the recorded calls are cleared, and a server left refusing connections accepts them again.
A complete example¶
version: 2
app:
command: go run ./cmd/server
port: 8080
ready:
type: http
path: /health
env:
USERS_ADDR: "{{.users.address}}"
resources:
api:
type: http-client
base_url: http://localhost:8080
users:
type: grpc-server
options:
port: 9995
descriptor_set: proto/users.binpb
Feature: Profile page
Scenario: The user's token reaches the user service
Given "users" stub "users.v1.UserService/GetUser" with metadata "authorization" "Bearer alice" returns json:
"""
{"id": "42", "name": "Alice"}
"""
And "api" header "Authorization" is "Bearer alice"
When "api" sends "GET" to "/profile/42"
Then "api" response status is "200"
And "users" last "users.v1.UserService/GetUser" request json contains:
"""
{"id": "42"}
"""
See gRPC Server Steps for every step.
Limitations¶
- Unary and server-streaming methods only. A client-streaming or
bidirectional method cannot be stubbed yet: the step says so, and a call to
one is recorded and answered
UNIMPLEMENTED. Stubbing those needs steps for what arrives over time, which is a design of its own. - No response metadata or trailers on stubbed answers yet.
- Plaintext only: no TLS.
- No
.protofiles directly. Build a descriptor set from them with buf or protoc.