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.

Leave a Comment

Your email address will not be published. Required fields are marked *