Testing a gRPC service

前端 未结 8 500
旧巷少年郎
旧巷少年郎 2020-12-12 15:05

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

相关标签:
8条回答
  • 2020-12-12 15:17

    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.

    0 讨论(0)
  • 2020-12-12 15:17

    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

    0 讨论(0)
  • 2020-12-12 15:18

    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.

    0 讨论(0)
  • 2020-12-12 15:26

    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.

    0 讨论(0)
  • 2020-12-12 15:27

    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.

    0 讨论(0)
  • 2020-12-12 15:27

    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)
            }
    
        })
    }
    
    0 讨论(0)
提交回复
热议问题