Steps

The grpc step

A grpc step makes one call: unary or streaming, described by .proto files that vero compiles when the plan loads. vero never asks a server for its schema, so a misspelled method or a message field the request type lacks is a load error, before any socket opens. The call runs under the step's deadline and uses the run's TLS settings, --insecure-skip-verify, --ca-cert and the client certificate, unless plaintext is set. Plan format has the keys every step shares; this page has the ones a grpc step adds.

Key Type Meaning
target string host:port, templated
plaintext bool no TLS; off by default, so a call uses TLS with the run's settings
method string package.Service/Method, a literal
protos list of strings the .proto files that describe the method, relative to the plan
importPaths list of strings where imports are found, relative to the plan; the plan's directory is always searched
message any the request, written as JSON: strings templated, numbers as written; for a method that takes one message
send list of objects the requests, in order, each written like message; for a method that takes a stream
expect object optional, for a method that returns a stream: { count, timeout } as for ws, stop collecting early
metadata map of strings sent with the call, templated

A plan

grpc.yaml calls the vero.test.Orders service that the two files in examples/grpc/ describe: one unary call, then one server stream. orders.proto imports types.proto, which sits beside it, so the step names grpc in importPaths.

  - name: get-order
    steps:
      - grpc:
          target: "{{ env.grpc }}"
          plaintext: true
          method: vero.test.Orders/GetOrder
          protos: [grpc/orders.proto]
          importPaths: [grpc]
          message: { id: 7 }
        assert:
          - code == "OK"
          - body.id == 7
          - body.status == "pending"
          - body.items[0].sku == "ABC-1"
          - body.total.cents == 1010
          - body.qty == 0
          - headers["x-served-by"] == "fixture"

No shipped fixture speaks gRPC. The repository test runs this plan against a server it starts in process, and real ws and gRPC servers runs the step against grpcbin. Against the in-process server:

run 01M3ZS1HYJ3RV49T7FA519H2MH  grpc.yaml  --jobs 4  lifecycle isolated

2 passed  0 failed  0 errored  0 timed out  0 skipped  0 blocked  0 not run   (2 tasks, 2 requests, 0.0s)

Each call counts as one request in the summary, and vero plan counts it among the requests the plan sends.

The method's shape

The shape of the method decides which keys apply, and the wrong one is a load error:

Method Goes out Comes back
unary message body
server streaming message messages
client streaming send body
bidirectional send messages

send goes out in order and then the client half-closes. No send waits for a reply, and none can be templated from one: the plan is data, not a conversation. Sends and reads run at the same time, so a server that answers each message before it takes the next works. send holds at most 1000 messages, and each is a JSON object.

A server stream is collected until the server ends it, up to 1000 messages. A stream that sends more ends the step errored with the stream sent more than 1000 messages; set expect: { count, timeout } to stop collecting earlier. With expect, collection stops after count messages or at timeout from the start of the call, whichever comes first, and the step also asserts messages count <count>, so fewer messages is a failure that shows what arrived. expect.count is 1 to 1000, and expect.timeout has to be shorter than the step's deadline (30 s unless the step sets timeout), or the load error says expect.timeout 2s does not fit in the step's 1s deadline; raise the step's timeout.

When vero stops a stream this way, the call is cancelled and the server has given no status: code is nil, and there are no trailers. A plan cannot tell from that whether the server would have failed later; it did not say. A stream the server ended on its own just as the window closed keeps the server's status.

Messages, in and out

A request message is written as JSON. Keys match the proto's JSON names first and the field names second, through nested messages and lists, and a key the type lacks is refused at load:

grpc.yaml:22: tasks.get-order.steps[0].grpc.message: message.skuu: vero.test.GetOrderRequest has no field skuu

Strings in a message are templated and numbers are sent as written. A templated value is checked when it renders, so a rendered message the type cannot hold ends the step errored with the message does not fit vero.test.GetOrderRequest: ..., or send[2] does not fit ... for a stream.

A response reaches assertions with the proto's JSON field names and every field present, so body.qty == 0 holds on a message that never set qty. A message-typed field that was not set is nil. Enums come as their names, bytes as base64, and every integer kind and float is an exact number: body.id == 9007199254740993 holds and body.id == 9007199254740992 does not. NaN and +Inf arrive as those strings, since they are not numbers an assertion can compare.

Names in scope

Name Meaning
code the status as the gRPC spec writes it: OK, NOT_FOUND, UNAVAILABLE, DEADLINE_EXCEEDED; nil when vero stopped the stream. A code the spec does not name is CODE_<number>
message the status message
body the one response of a method that returns one message; nil unless the code is OK, and nil for a method that returns a stream
messages a stream's responses in arrival order, each with body and at, the time since the call started
headers, trailers the server's metadata; no trailers when vero stopped the stream
duration.first when the first streamed message arrived; n/a for a unary call or an empty stream
duration.total the whole call
env, tasks, run as in every step

Unless one of its own assertions reads code, the step also asserts code == "OK", or code == nil or code == "OK" when it has expect. A non-OK status the plan does not mention fails the step, the way an http step with no status assertion would not let a 500 through unnoticed. A plan that expects an error says so: code == "NOT_FOUND" and message == "order 404 not found" pass against a server that answers that way.

No server answering at all is errored, not a code. Assertions has the expression syntax, and duration on its own is refused: name duration.first or duration.total.

What a failure shows

The failure block prints the call, what went out, the headers the server sent, the response or each streamed message with its arrival time, and the trailers:

FAIL  get-order › step 1 › assert #3                              grpc.yaml:26

  GRPC /vero.test.Orders/GetOrder 127.0.0.1:50051                 OK  1.7ms
    > {"id":7}
    < content-type: application/grpc
    < x-served-by: fixture
    < {"id":7,"items":[{"sku":"ABC-1"}],"qty":0,"status":"pending","total":{"cents":1010,"currency":"EUR"}}

  assert  body.status == "shipped"    failed
          body.status = "pending"
          grpc.yaml:26

A stream that sent fewer messages than expect.count asked for, by expect.timeout:

  GRPC /vero.test.Orders/WatchOrder 127.0.0.1:50051               OK  1.1ms
    > {"id":2}
    < content-type: application/grpc
    < {"id":1,"items":[],"qty":0,"status":"pending","total":null}   at 0.9ms
    < {"id":2,"items":[],"qty":0,"status":"packed","total":null}   at 1.0ms

  assert  messages count 3    failed
          messages = [{"body":{"id":1,"items":[],"qty":0,"status":"pending","total":null},"at":0.950},{"body":{"id":2,"items":[],"qty":0,"status":"packed","total":null},"at":0.961}]
          2 of 3 messages arrived within 300ms
          grpc.yaml:41

The status column reads stopped by vero, no status when expect ended the stream, and no answer when no server answered:

ERROR  get-order › step 1                                         grpc.yaml

  connecting to 127.0.0.1:50051: connection error: desc = "transport: Error while dialing: dial tcp 127.0.0.1:50051: connect: connection refused"

  GRPC /vero.test.Orders/GetOrder 127.0.0.1:50051                 no answer  2.6ms
    > {"id":7}

A call the step's deadline cut off is timed_out with deadline exceeded calling /vero.test.Orders/GetOrder. The clock decides that, not the server: a server can answer DEADLINE_EXCEEDED first, because grpc-go sends it the deadline, and that answer is vero's deadline, not a status the server chose.

The event log carries the same call as a grpc event: the target, the method, what was sent, the code and status message, the response or each received message with its time, headers and trailers (vero.events/v1).

Load errors

Plan Load error
no target a grpc step needs a target, host:port
no method a grpc step needs a method, package.Service/Method
no protos a grpc step needs protos, the .proto files that describe its method, relative to the plan
a {{ in method, protos or importPaths method, protos and importPaths are literals, not templates
method: Orders method is package.Service/Method, got "Orders"
a method the service lacks service vero.test.Orders has no method GetOrders; it has GetOrder, WatchOrder, AddItems, Echo
a service no proto declares no service vero.test.Order in grpc/orders.proto
a proto that does not compile the compiler's message, at .protos
send on a unary or server-streaming method vero.test.Orders/WatchOrder takes one message; write it as message, not send
message on a client-streaming method vero.test.Orders/AddItems takes a stream; write its messages as send: [ ... ], not message
expect on a method that returns one message vero.test.Orders/GetOrder returns one message; expect applies to a method that returns a stream
more than 1000 sends send holds at most 1000 messages, got 1001
a send that is not an object a message is a JSON object

This page is docs/content/steps/grpc.md in the repository.

verodocs