Testing a gRPC service

GoGrpc

Go Problem Overview


I'd like to test a gRPC service written in Go. The example I'm using is the Hello World server example from the grpc-go repo.

The protobuf definition is as follows:

syntax = "proto3";

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;
}

And the type in the greeter_server main is:

// server is used to implement helloworld.GreeterServer.
type server struct{}

// SayHello implements helloworld.GreeterServer
func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
	return &pb.HelloReply{Message: "Hello " + in.Name}, nil
}

I've looked for examples but I couldn't find any on how to implement tests for a gRPC service in Go.

Go Solutions


Solution 1 - Go

I think you're looking for the google.golang.org/grpc/test/bufconn package to help you avoid starting up a service with a real port number, but still allowing testing of streaming RPCs.

import "google.golang.org/grpc/test/bufconn"

const bufSize = 1024 * 1024

var lis *bufconn.Listener

func init() {
    lis = bufconn.Listen(bufSize)
    s := grpc.NewServer()
    pb.RegisterGreeterServer(s, &server{})
    go func() {
        if err := s.Serve(lis); err != nil {
            log.Fatalf("Server exited with error: %v", err)
        }
    }()
}

func bufDialer(context.Context, string) (net.Conn, error) {
    return lis.Dial()
}

func TestSayHello(t *testing.T) {
    ctx := context.Background()
    conn, err := grpc.DialContext(ctx, "bufnet", grpc.WithContextDialer(bufDialer), grpc.WithInsecure())
    if err != nil {
        t.Fatalf("Failed to dial bufnet: %v", err)
    }
    defer conn.Close()
    client := pb.NewGreeterClient(conn)
    resp, err := client.SayHello(ctx, &pb.HelloRequest{"Dr. Seuss"})
    if err != nil {
        t.Fatalf("SayHello failed: %v", err)
    }
    log.Printf("Response: %+v", resp)
    // Test for output here.
}

The benefit of this approach is that you're still getting network behavior, but over an in-memory connection without using OS-level resources like ports that may or may not clean up quickly. And it allows you to test it the way it's actually used, and it gives you proper streaming behavior.

I don't have a streaming example off the top of my head, but the magic sauce is all above. It gives you all of the expected behaviors of a normal network connection. The trick is setting the WithDialer option as shown, using the bufconn package to create a listener that exposes its own dialer. I use this technique all the time for testing gRPC services and it works great.

Solution 2 - Go

If you want to verify that the implementation of the gRPC service does what you expect, then you can just write standard unit tests and ignore networking completely.

For example, make greeter_server_test.go:

func HelloTest(t *testing.T) {
	s := server{}
	
	// set up test cases
	tests := []struct{
		name string
		want string
	} {
		{
			name: "world",
			want: "Hello world",
		},
		{
			name: "123",
			want: "Hello 123",
		},
	}
	
	for _, tt := range tests {
		req := &pb.HelloRequest{Name: tt.name}
		resp, err := s.SayHello(context.Background(), req)
		if err != nil {
			t.Errorf("HelloTest(%v) got unexpected error")
		}
		if resp.Message != tt.want {
			t.Errorf("HelloText(%v)=%v, wanted %v", tt.name, resp.Message, tt.want)
		}
	}
}

I might've messed up the proto syntax a bit doing it from memory, but that's the idea.

Solution 3 - Go

Here is possibly a simpler way of just testing a streaming service. Apologies if there are any typo's as I am adapting this from some running code.

Given the following definition.

rpc ListSites(Filter) returns(stream sites) 

With the following server side code.

// ListSites ...
func (s *SitesService) ListSites(filter *pb.SiteFilter, stream pb.SitesService_ListSitesServer) error {
	for _, site := range s.sites {
		if err := stream.Send(site); err != nil {
			return err
		}
	}
	return nil
}

Now all you have to do is mock the pb.SitesService_ListSitesServer in your tests file.

type mockSiteService_ListSitesServer struct {
	grpc.ServerStream
	Results []*pb.Site
}

func (_m *mockSiteService_ListSitesServer) Send(site *pb.Site) error {
	_m.Results = append(_m.Results, site)
	return nil
}

This responds to the .send event and records the sent objects in .Results which you can then use in your assert statements.

Finally you call the server code with the mocked immplementation of pb.SitesService_ListSitesServer.

func TestListSites(t *testing.T) {
    s := SiteService.NewSiteService()
    filter := &pb.SiteFilter{}

	mock := &mockSiteService_ListSitesServer{}
    s.ListSites(filter, mock)

	assert.Equal(t, 1, len(mock.Results), "Sites expected to contain 1 item")
}

No it doesn't test the entire stack but it does allow you to sanity check your server side code without the hassle of running up a full gRPC service either for real or in mock form.

Solution 4 - Go

I came up with the following implementation which may not be the best way of doing it. Mainly using the TestMain function to spin up the server using a goroutine like that:

const (
	port = ":50051"
)

func Server() {
	lis, err := net.Listen("tcp", port)
	if err != nil {
		log.Fatalf("failed to listen: %v", err)
	}
	s := grpc.NewServer()
	pb.RegisterGreeterServer(s, &server{})
	if err := s.Serve(lis); err != nil {
		log.Fatalf("failed to serve: %v", err)
	}
}
func TestMain(m *testing.M) {
	go Server()
	os.Exit(m.Run())
}

and then implement the client in the rest of the tests:

func TestMessages(t *testing.T) {

	// Set up a connection to the Server.
	const address = "localhost:50051"
	conn, err := grpc.Dial(address, grpc.WithInsecure())
	if err != nil {
		t.Fatalf("did not connect: %v", err)
	}
	defer conn.Close()
	c := pb.NewGreeterClient(conn)

	// Test SayHello
	t.Run("SayHello", func(t *testing.T) {
		name := "world"
		r, err := c.SayHello(context.Background(), &pb.HelloRequest{Name: name})
		if err != nil {
			t.Fatalf("could not greet: %v", err)
		}
		t.Logf("Greeting: %s", r.Message)
		if r.Message != "Hello "+name {
			t.Error("Expected 'Hello world', got ", r.Message)
		}

	})
}

Solution 5 - Go

There are many ways you can choose to test a gRPC service. You may choose to test in different ways depending on the kind of confidence you would like to achieve. Here are three cases that illustrate some common scenarios.

Case #1: I want to test my business logic

In this case you are interested in the logic in the service and how it interacts with other components. The best thing to do here is write some unit tests.

There is a good introduction to unit testing in Go by Alex Ellis. If you need to test interactions then GoMock is the way to go. Sergey Grebenshchikov wrote a nice GoMock tutorial.

The answer from Omar shows how you could approach unit testing this particular SayHello example.

Case #2: I want to manually test the API of my live service over the wire

In this case you are interested in doing manually exploratory testing of your API. Typically this is done to explore the implementation, check edge cases and gain confidence that your API behaves as expected.

You will need to:

  1. Start your gRPC server
  2. Use an over the wire mocking solution to mock any dependencies you have e.g. if your gRPC service under test makes a gRPC call to another service. For example you can use Traffic Parrot.
  3. Use a gRPC API testing tool. For example you can use a gRPC CLI.

Now you can use your mocking solution to simulate real and hypothetical situations while observing the behaviour on the service under test by using the API testing tool.

Case #3: I want automated over the wire testing of my API

In this case you are interested in writing automated BDD style acceptance tests that interact with the system under test via the over the wire gRPC API. These tests are expensive to write, run and maintain and should be used sparingly, keeping in mind the testing pyramid.

The answer from thinkerou shows how you can use karate-grpc to write those API tests in Java. You can combine this with the Traffic Parrot Maven plugin to mock any over the wire dependencies.

Solution 6 - Go

BTW: as a new contributor, I cannot add to comments. So I am adding a new answer here.

I can confirm that the @Omar approach works for testing a non-streaming gRPC service by testing via the interface without a running service.

However this approach will not work for streams. Since gRPC supports bidirectional streams, it is necessary to fire-up the service and connected to it via the network layer to do testing for streams.

The approach that @joscas takes works for gRPC streams (even though the helloworld sample code does not use streams) using a goroutine to start the service. However, I noticed that on Mac OS X 10.11.6 that it does not release the port used by the service consistently when called from a goroutine (As I understand, the service will block the goroutine and perhaps does not exit cleanly). By firing up a separate process for the service to run in, using 'exec.Command', and killing it before finishing, the port is released consistently.

I uploaded a working test file for a gRPC service using streams to github: https://github.com/mmcc007/go/blob/master/examples/route_guide/server/server_test.go

You can see the tests running on travis: https://travis-ci.org/mmcc007/go

Please let me know if any suggestions on how to improve testing for gRPC services.

Solution 7 - Go

As a new contributor, I can not comment so I am adding here as an answer.

The @shiblon answer is the best way to test your service. I am the maintainer of the grpc-for-production and one of the features is an in processing server which makes it easier to work with bufconn.

Here one example of testing the greeter service

var server GrpcInProcessingServer

func serverStart() {
	builder := GrpcInProcessingServerBuilder{}
	builder.SetUnaryInterceptors(util.GetDefaultUnaryServerInterceptors())
	server = builder.Build()
	server.RegisterService(func(server *grpc.Server) {
		helloworld.RegisterGreeterServer(server, &testdata.MockedService{})
	})
	server.Start()
}

//TestSayHello will test the HelloWorld service using A in memory data transfer instead of the normal networking
func TestSayHello(t *testing.T) {
	serverStart()
	ctx := context.Background()
	clientConn, err := GetInProcessingClientConn(ctx, server.GetListener(), []grpc.DialOption{})
	if err != nil {
		t.Fatalf("Failed to dial bufnet: %v", err)
	}
	defer clientConn.Close()
	client := helloworld.NewGreeterClient(clientConn)
	request := &helloworld.HelloRequest{Name: "test"}
	resp, err := client.SayHello(ctx, request)
	if err != nil {
		t.Fatalf("SayHello failed: %v", err)
	}
	server.Cleanup()
	clientConn.Close()
	assert.Equal(t, resp.Message, "This is a mocked service test")
}

You can find this example here

Solution 8 - Go

you can use karate-grpc to test grpc service, you only need to post your proto jar and grpc server ip/port. karate-grpc build based on karate and polyglot.

One hello-world example:

Feature: grpc helloworld example by grpc dynamic client

  Background:
    * def Client = Java.type('com.github.thinkerou.karate.GrpcClient')
    * def client = Client.create('localhost', 50051)

  Scenario: do it
    * def payload = read('helloworld.json')
    * def response = client.call('helloworld.Greeter/SayHello', payload)
    * def response = JSON.parse(response)
    * print response
    * match response[0].message == 'Hello thinkerou'
    * def message = response[0].message

    * def payload = read('again-helloworld.json')
    * def response = client.call('helloworld.Greeter/AgainSayHello', payload)
    * def response = JSON.parse(response)
    * match response[0].details == 'Details Hello thinkerou in BeiJing'

About the example of karate-grpc comment:

enter image description here

And it will generate beautiful report, like:

enter image description here

More details please see: https://thinkerou.com/karate-grpc/

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionjoscasView Question on Stackoverflow
Solution 1 - GoshiblonView Answer on Stackoverflow
Solution 2 - GoOmarView Answer on Stackoverflow
Solution 3 - GoSimon BView Answer on Stackoverflow
Solution 4 - GojoscasView Answer on Stackoverflow
Solution 5 - GoLiam WilliamsView Answer on Stackoverflow
Solution 6 - GommccabeView Answer on Stackoverflow
Solution 7 - GoAlexsandro SouzaView Answer on Stackoverflow
Solution 8 - GothinkerouView Answer on Stackoverflow