gRPC Reflection and Testing
Testing gRPC services requires tools and techniques that differ from REST testing. Server reflection lets tools discover your API without needing the proto files. This topic covers how to expose reflection, test services with command-line tools, and write automated unit and integration tests.
What Is gRPC Server Reflection?
Normally, a gRPC client needs the compiled proto definitions to call a service. Reflection is an optional service you register on the server that exposes those definitions at runtime. Tools like grpcurl and Postman use reflection to let you call any gRPC method without a proto file — exactly like using a browser's network tab to call a REST API.
Without reflection: Developer → needs .proto file → compile it → write client → make call (5–10 minutes of setup per service) With reflection: Developer → grpcurl localhost:50051 list → see all services instantly Developer → grpcurl ... call → get a response in seconds
Enabling Server Reflection in Go
import (
"google.golang.org/grpc/reflection"
)
func main() {
grpcServer := grpc.NewServer()
// Register your business services
pb.RegisterOrderServiceServer(grpcServer, &orderServer{})
pb.RegisterUserServiceServer(grpcServer, &userServer{})
// Register reflection — one line, enables all tooling
reflection.Register(grpcServer)
lis, _ := net.Listen("tcp", ":50051")
grpcServer.Serve(lis)
}
Important: enable reflection in development and staging.
Disable it in production to avoid leaking your API schema.
Use a build flag or environment variable:
if os.Getenv("GRPC_REFLECTION") == "true" {
reflection.Register(grpcServer)
}
grpcurl — curl for gRPC
grpcurl is the most widely used command-line tool for gRPC. It works like curl for REST APIs.
Install:
go install github.com/fullstorydev/grpcurl/cmd/grpcurl@latest
List all services on the server:
grpcurl -plaintext localhost:50051 list
Output:
grpc.health.v1.Health
grpc.reflection.v1alpha.ServerReflection
order.OrderService
user.UserService
List all methods in a service:
grpcurl -plaintext localhost:50051 list order.OrderService
Output:
order.OrderService.CreateOrder
order.OrderService.GetOrder
order.OrderService.ListOrders
order.OrderService.UpdateStatus
Describe a message type:
grpcurl -plaintext localhost:50051 describe order.GetOrderRequest
Output:
order.GetOrderRequest is a message:
message GetOrderRequest {
int32 order_id = 1;
bool include_items = 2;
}
Call a method (JSON input → JSON output):
grpcurl -plaintext \
-d '{"order_id": 42, "include_items": true}' \
localhost:50051 \
order.OrderService/GetOrder
Output:
{
"orderId": 42,
"status": "PROCESSING",
"items": [
{"productId": 7, "quantity": 2}
]
}
Call with metadata (e.g. auth token):
grpcurl -plaintext \
-H "authorization: Bearer eyJhbGc..." \
-d '{"user_id": 1}' \
localhost:50051 \
user.UserService/GetUser
Call a TLS server:
grpcurl \
-cacert ca.crt \
-d '{"order_id": 99}' \
server.example.com:443 \
order.OrderService/GetOrder
Postman for gRPC
Postman supports gRPC natively since version 9.7. You can import proto files or use server reflection to browse methods and send calls with a graphical interface.
Steps to use Postman with gRPC: 1. Create a new request → select "gRPC" 2. Enter URL: localhost:50051 3. Click "Import .proto file" OR enable "Use server reflection" 4. Select a service and method from the dropdown 5. Fill in the request message in the JSON editor 6. Click "Invoke" 7. See the response in the right panel Good for: • Exploring an unfamiliar service • Manual testing during development • Sharing test cases with non-technical teammates via Postman collections
Unit Testing gRPC Servers
Unit tests call your handler functions directly, with no network involved. Pass a fake or mock for any dependencies like databases.
// order_server_test.go
package main
import (
"context"
"testing"
pb "myapp/gen/order"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
// Fake database for tests
type fakeOrderDB struct {
orders map[int32]*pb.Order
}
func (db *fakeOrderDB) Get(id int32) (*pb.Order, bool) {
o, ok := db.orders[id]
return o, ok
}
func TestGetOrder_Found(t *testing.T) {
// Arrange
srv := &orderServer{
db: &fakeOrderDB{
orders: map[int32]*pb.Order{
42: {OrderId: 42, Status: pb.Order_PROCESSING},
},
},
}
// Act
resp, err := srv.GetOrder(context.Background(),
&pb.GetOrderRequest{OrderId: 42})
// Assert
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if resp.OrderId != 42 {
t.Errorf("expected order 42, got %d", resp.OrderId)
}
}
func TestGetOrder_NotFound(t *testing.T) {
srv := &orderServer{db: &fakeOrderDB{orders: map[int32]*pb.Order{}}}
_, err := srv.GetOrder(context.Background(),
&pb.GetOrderRequest{OrderId: 999})
st, _ := status.FromError(err)
if st.Code() != codes.NotFound {
t.Errorf("expected NOT_FOUND, got %v", st.Code())
}
}
Integration Testing with bufconn
bufconn is a gRPC package that creates an in-memory network connection. Integration tests run a full gRPC server and client in the same process — no network port needed, no socket conflicts between tests.
// integration_test.go
import (
"context"
"net"
"testing"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/test/bufconn"
pb "myapp/gen/order"
)
const bufSize = 1024 * 1024
func startTestServer(t *testing.T) (*bufconn.Listener, func()) {
t.Helper()
lis := bufconn.Listen(bufSize)
grpcSrv := grpc.NewServer()
pb.RegisterOrderServiceServer(grpcSrv, &orderServer{
db: &fakeOrderDB{orders: map[int32]*pb.Order{
1: {OrderId: 1, Status: pb.Order_PENDING},
}},
})
go grpcSrv.Serve(lis)
cleanup := func() {
grpcSrv.Stop()
lis.Close()
}
return lis, cleanup
}
func TestCreateAndGetOrder(t *testing.T) {
lis, cleanup := startTestServer(t)
defer cleanup()
// Dial using bufconn — no real network
conn, err := grpc.NewClient("bufnet",
grpc.WithContextDialer(func(ctx context.Context, _ string) (net.Conn, error) {
return lis.DialContext(ctx)
}),
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
if err != nil {
t.Fatalf("failed to dial bufconn: %v", err)
}
defer conn.Close()
client := pb.NewOrderServiceClient(conn)
// Test GetOrder
resp, err := client.GetOrder(context.Background(),
&pb.GetOrderRequest{OrderId: 1})
if err != nil {
t.Fatalf("GetOrder failed: %v", err)
}
if resp.Status != pb.Order_PENDING {
t.Errorf("expected PENDING status, got %v", resp.Status)
}
}
Testing Streaming RPCs
Testing server streaming:
stream, err := client.StreamOrders(ctx,
&pb.StreamOrdersRequest{UserId: 42})
if err != nil {
t.Fatal(err)
}
var orders []*pb.Order
for {
order, err := stream.Recv()
if err == io.EOF {
break // stream closed normally
}
if err != nil {
t.Fatal(err)
}
orders = append(orders, order)
}
if len(orders) != 3 {
t.Errorf("expected 3 orders, got %d", len(orders))
}
Tools Reference
┌────────────────────┬────────────────────────────────────────────────┐ │ Tool │ Use For │ ├────────────────────┼────────────────────────────────────────────────┤ │ grpcurl │ CLI exploration and manual testing │ │ Postman │ GUI manual testing and collection sharing │ │ Buf CLI │ Proto linting, breaking change detection │ │ ghz │ Load testing (like ab/wrk for gRPC) │ │ grpc-health-probe │ Health check CLI and Kubernetes probes │ │ Evans │ Interactive gRPC CLI (REPL style) │ │ Kreya │ GUI client, similar to Postman │ └────────────────────┴────────────────────────────────────────────────┘
Summary
Server reflection lets tools discover your API without proto files — enable it in development, disable it in production. grpcurl lets you call any gRPC method from the command line in seconds. Write unit tests by calling handler methods directly with fake dependencies. Use bufconn for integration tests that run a full gRPC server in memory with no port conflicts. Test streaming RPCs by calling stream.Recv() in a loop until io.EOF.
