93

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.

joscas
  • 7,474
  • 5
  • 39
  • 59

8 Answers8

163

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.

Xia Li
  • 3
  • 2
shiblon
  • 1,916
  • 1
  • 14
  • 5
  • 6
    Note this package wasn't available at the time of the question. That's why @omar 's answer was accepted initially. – joscas Jun 05 '20 at 09:34
  • This approach is especially useful if you need to test that the GRPC error-handling, error-wrapping and returned status is working as you are expecting. – Petr Sep 14 '21 at 13:41
  • Note that this approach does not shut down the server, which can cause issues if you need to start and stop the server in between tests. – Woody1193 Oct 21 '22 at 02:25
  • Thanks for the answer. Just a side comment, that logic causes quite some confusion as the the magic string of `bufnet` when I first read the code, I have to search around to find the answer. A better approach is `bufconn` should provide a constant to it instead of letting every application hard code this magic string. – Jianwu Chen May 05 '23 at 18:23
58

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.

joscas
  • 7,474
  • 5
  • 39
  • 59
Omar
  • 1,329
  • 11
  • 9
22

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.

Simon B
  • 231
  • 2
  • 3
17

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.

Liam Williams
  • 676
  • 9
  • 16
15

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

    })
}
joscas
  • 7,474
  • 5
  • 39
  • 59
  • 5
    Incidentally, instead of defining `port` and `address` variables, you can also leave the port empty, like `net.Listen("tcp", ":")`, and use `lis.Addr().String()` to get the automatically chosen address (cf. https://godoc.org/net#Listen). This precludes the test failing because the address is already in use. – Kurt Peek Jul 21 '20 at 21:05
9

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.

mmccabe
  • 2,259
  • 1
  • 24
  • 25
3

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

Alexsandro Souza
  • 806
  • 7
  • 14
-1

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/

thinkerou
  • 1,781
  • 5
  • 17
  • 28