· updated
How to test gRPC services
Testing gRPC starts with a schema, not a URL and an arbitrary JSON body. A client needs the service, method, and protobuf message types before it can encode a call. Once that information is loaded, the basic loop is familiar: configure an address, metadata, and a request, then inspect the response and status. Streaming, deadlines, and TLS are where the workflow becomes distinctly gRPC.
The examples below use grpcurl so they are easy to reproduce in a terminal. The same sequence works in graphical gRPC clients, including Tetiva: load reflection or .proto files, select a method, fill the message, and send the call.
Start with a small service definition
Consider an order service with unary and streaming methods:
syntax = "proto3";
package shop.v1;
service OrderService {
rpc GetOrder(GetOrderRequest) returns (Order);
rpc WatchOrders(WatchOrdersRequest) returns (stream Order);
rpc ImportOrders(stream CreateOrderRequest) returns (ImportSummary);
rpc Chat(stream ChatMessage) returns (stream ChatMessage);
}
message GetOrderRequest {
string id = 1;
}
message WatchOrdersRequest {
string customer_id = 1;
}
message CreateOrderRequest {
string customer_id = 1;
repeated string sku = 2;
}
message Order {
string id = 1;
string status = 2;
}
message ImportSummary {
int32 accepted = 1;
}
message ChatMessage {
string text = 1;
}
The complete unary method name is shop.v1.OrderService/GetOrder. The package is significant. Calling only OrderService/GetOrder can produce UNIMPLEMENTED even when the server is healthy.


Discover the API with server reflection
Server reflection lets a client request service lists and protobuf descriptors at runtime. It is usually the fastest setup for development because you do not have to locate a matching copy of every .proto file.
Check whether reflection is available:
grpcurl -plaintext localhost:50051 list
grpcurl -plaintext localhost:50051 list shop.v1.OrderService
grpcurl -plaintext localhost:50051 describe shop.v1.OrderService.GetOrder
If discovery succeeds, invoke the method directly:
grpcurl -plaintext \
-d '{"id":"ord_123"}' \
localhost:50051 \
shop.v1.OrderService/GetOrder
Production servers often disable reflection to avoid publishing their service catalog. In that case, provide source files and import roots:
grpcurl -plaintext \
-import-path ./proto \
-proto shop/v1/orders.proto \
-d '{"id":"ord_123"}' \
localhost:50051 \
shop.v1.OrderService/GetOrder
If the schema imports google/type/date.proto, the main file alone is not enough. Add the directory from which the path google/type/date.proto can be resolved.
Generate a request from the message type
Do not infer the body from a method name. Inspect its input descriptor and follow protobuf JSON mapping. Field names are normally written as customerId, although many tools also accept the original customer_id. JSON representations commonly encode int64 as a string, enums by name, bytes as Base64, and Timestamp in RFC 3339 format.
A useful starter body for CreateOrderRequest is:
{
"customerId": "cus_42",
"sku": ["book-1", "pen-2"]
}
Schema-based generation saves typing, but its output is only a draft. Empty strings, zeroes, and the first enum value may be valid protobuf while failing business validation. Tetiva can generate an example body from the loaded schema; replace placeholders with values that make sense for the domain before sending it.
Know the four call shapes
gRPC defines four method shapes:
- Unary: one request and one response. It is the easiest form to repeat in a smoke suite.
- Server streaming: one request followed by multiple responses. Test event order, clean completion, and client cancellation.
- Client streaming: multiple requests followed by one response. The client must close its sending side, or the server may keep waiting.
- Bidirectional streaming: both sides send independently. Avoid assuming that every response maps one-to-one to the most recently sent message.
grpcurl prints a server stream as messages arrive:
grpcurl -plaintext \
-d '{"customerId":"cus_42"}' \
localhost:50051 \
shop.v1.OrderService/WatchOrders
An interactive or graphical client is often clearer for client and bidirectional streams. Open the call, send several messages, then close the client half of the stream. Define expected events and a maximum wait; otherwise a stalled test is indistinguishable from a slow one.
Send authentication through metadata
Metadata consists of key-value pairs carried over HTTP/2. Keys use lowercase ASCII; binary keys end in -bin. A bearer token and correlation ID can be sent like this:
grpcurl -plaintext \
-H 'authorization: Bearer dev-token' \
-H 'x-request-id: test-001' \
-d '{"id":"ord_123"}' \
localhost:50051 \
shop.v1.OrderService/GetOrder
For UNAUTHENTICATED, verify that the credential is present and correctly formatted. PERMISSION_DENIED more often means the caller was identified but cannot perform the operation. Keep real tokens in a local environment or secret store rather than committing them with a collection.
Put a deadline on every test call
A test should never wait forever for a lost packet, stalled dependency, or stream that was not closed.
grpcurl -plaintext \
-max-time 3 \
-d '{"id":"ord_123"}' \
localhost:50051 \
shop.v1.OrderService/GetOrder
DEADLINE_EXCEEDED does not prove that the server performed no work. An operation may complete after the client stops waiting. For mutations, use an idempotency key and correlate server logs with a request ID. A deadline that is too tight creates flaky tests; one that is too generous hides regressions. Derive it from the latency budget of the operation under test.
Diagnose TLS and mTLS failures
Use -plaintext only for a server without TLS. Remove it for a standard secured endpoint:
grpcurl api.example.com:443 list
Supply a private CA when necessary, plus a client certificate and key for mTLS:
grpcurl \
-cacert ./certs/ca.pem \
-cert ./certs/client.pem \
-key ./certs/client-key.pem \
api.example.com:443 list
An error saying the certificate is valid for a different name points to hostname verification. Connecting by IP instead of the certificate’s DNS name can also change TLS SNI and proxy routing. Disabling certificate verification may help diagnose a local setup, but it should never become a saved production configuration.
A repeatable testing order
Confirm the address and TLS mode first. Load descriptors through reflection or local files, verify the fully qualified method name, generate a body from the input type, and add metadata plus a realistic deadline. For streams, test sending, receiving, half-close, cancellation, and completion separately. Finally, save the successful call as a collection test so a manual discovery becomes a repeatable check.