gRPC client stub

Chintan Gandhi
4 min readApr 17, 2023

--

Recently while writing unit test cases for an enterprise application. I had a straightforward case where business logic depends on a response from another application. Function, to be tested, makes gRPC call and proceeds with logic. In my unit test, I had to initialize the gRPC client, and the connection to the server worked, but it is not ideal for a couple of reasons. First, CICD builds servers that will not have access to external servers, and second, for testing different scenarios I needed a remote server to respond to specific messages in response. To fix this, there were 3 approaches, each with its own pros and cons.

In this particular article, I will simply be demonstrating gRPC client stub. To see the difference between the steps of testing by connecting to external server vs stub, we will first see how to test it with connecting with external server.

Testing with remote/external server

For demonstration purposes, I’ll be using helloworld.proto file.

syntax = "proto3";

option go_package = "google.golang.org/grpc/examples/helloworld/helloworld";

package helloworld;

// The greeting service definition.
service Greeter {
// Sends a greeting
rpc SayHello (HelloRequest) returns (HelloReply) {}
}

// The request message containing the user's name.
message HelloRequest {
string name = 1;
}

// The response message containing the greetings
message HelloReply {
string message = 1;
}

Generate the source code

protoc --go_out=. --go_opt=paths=source_relative \
--go-grpc_out=. --go-grpc_opt=paths=source_relative \
helloworld.proto

gRPC Connections and client initialisation.

There are 2 steps needed to connect to gRPC Server

  1. Creating client connection.
  2. Creating gRPC client using the above connection

The sample demo file to do so looks like this below. The actual production connection will be much more involved, especially for a secured connection.

package dao

import (
"context"
"godemo/src/helloworld"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"log"
)

type GrpcClient struct {
client *helloworld.GreeterClient
}

var addr = "localhost:50051"

func NewGrpcClient(ctx context.Context) *helloworld.GreeterClient {

opts := []grpc.DialOption{
grpc.WithTransportCredentials(insecure.NewCredentials()),
}
// 1. Creating client connection
conn, err := grpc.DialContext(ctx, addr, opts...)
if err != nil {
log.Fatalf("failed to connect to server %q: %v", addr, err)
}

// 2. Creating gRPC client using the above connection
client := helloworld.NewGreeterClient(conn)

return &GrpcClient{
Client: &client,
}

}

The pointer to the client is wrapped in an object and returned. The function, using the above object calls the remote method.

package dao

import (
"context"
"godemo/helloworld"
)

func (dao GrpcClient) MyFunc(ctx context.Context, name string) (*helloworld.HelloReply, error) {
return (*dao.Client).SayHello(ctx, &helloworld.HelloRequest{Name: name})
}

To test the above function (“MyFunc”), we would traditionally test by calling retrieving the object which wraps the gRPC client and making an actual call.

package grpc_test

import (
"context"
"fmt"
"godemo/dao"
"godemo/helloworld"
"os"
"testing"
"time"
)

var gRPCClient *dao.GrpcClient

// 1. Starting Point
func TestMain(m *testing.M) {

mainCtx, cancel := context.WithCancel(context.Background()) // Main Context
defer cancel()

gRPCClient = dao.NewGrpcClient(mainCtx) // Initialize the gRPC client

code := m.Run() // Run all Test Cases
os.Exit(code)
}

func Test_Client(t *testing.T) {

requestCtx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()

// Call The function to be tested
helloReply, _ := gRPCClient.MyFunc(requestCtx, "Demo Message")

if reply.Message != "Demo Message" {
t.Errorf("Response Mismatch")
}
}

Testing with gRPC client Stub

To test it with the stub, let's quickly have a glance at autogenerated “helloworld_grpc.pb.go” using protoc command above.

Snippet from helloworld_grpc.pb.go

What we see here is two things

  1. We have an interface “GreeterClient” with a function named SayHello.
  2. We have a struct greeterClient which is initialized by calling the function “NewGreeterClient()” and their method “SayHello()” with the same definition as the function declaration in Interface “GreeterClient”

In our example above, our dao is passing a connection to the above NewGreeterClient function to get an object of the client (greeterClient).

So Its pretty simple, to stub, we need to just have another struct which returns us the object of type “GreeterClient”.

Below is our client stub

package stub_test

import (
"context"
"godemo/helloworld"
"google.golang.org/grpc"
)

type stubGreeterClient struct {
reply *helloworld.HelloReply
}

func NewStubGreeterClient(mockReply *helloworld.HelloReply) helloworld.GreeterClient {
return &stubGreeterClient{
reply: mockReply,
}
}

func (c *stubGreeterClient) SayHello(ctx context.Context, in *helloworld.HelloRequest, opts ...grpc.CallOption) (*helloworld.HelloReply, error) {
return c.reply, nil
}

Here, we have created a struct called “stubGreeterClient”. Function “NewStubGreeterClient” will return us the object of the struct. The only mandatory step here is we need to have a method “SayHello” which has the same definition and return type as declared in the interface “GreeterClient”. Struct stubGreeterClient can have none or any number of variables, there is no constraint there. here I have just one pointer variable reply of type “HelloReply” which is also the return type of the “SayHello” method. The reason is in my example, I'll be initializing the object with a pointer variable, and in my test case, I’ll pass the response, which I need to test my scenario.

How will my test case change?

Our test case will not reference to fetch stubGreeterClient Object and initialise same pointer of our GreeterClient

package stub_test

import (
"context"
"fmt"
"godemo/dao"
"godemo/helloworld"
"os"
"testing"
"time"
)

var gRPCClient *dao.GrpcClient
var mockResponse *helloworld.HelloReply

func TestMain(m *testing.M) {

stubClient := NewStubGreeterClient(mockResponse) // Create obj. of the gRPC stub client
gRPCClient = &dao.GrpcClient{Client: &stubClient} // Initialize the gRPC client

code := m.Run() // Run all Test Cases
os.Exit(code)
}

func Test_StubClient(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()

// Desired Response from gRPC server
desiredResponse := &helloworld.HelloReply{Message: "Stub Response"}

client := NewStubGreeterClient(desiredResponse)
reply, _ := client.SayHello(ctx, &helloworld.HelloRequest{Name: "Hello"})

if reply.Message != "Stub Response" {
t.Errorf("Response Mismatch")
}
}

If you now observe the 2 test cases

  1. func Test_Client(t *testing.T)
  2. func Test_Stub(t *testing.T)

The function call remains the same, gRPC client object remains the same. In Test_Client(t *testing.T) we do now have the flexibility to mock our response and design our test cases.

This approach is best when our focus is not testing our gRPC client but its our business logic. I’ll post a separate article on how can we stub server.

Happy Testing !!!

--

--