测试一个 gRPC 服务

huangapple go评论88阅读模式
英文:

Testing a gRPC service

问题

我想测试一个用Go编写的gRPC服务。我使用的示例是来自grpc-go仓库的Hello World服务器示例。

protobuf定义如下:

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

greeter_server的主函数中,类型为:

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

我已经寻找了示例,但是找不到关于如何在Go中为gRPC服务实现测试的示例。

英文:

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.

答案1

得分: 163

我认为你正在寻找google.golang.org/grpc/test/bufconn包,以帮助你避免使用真实的端口号启动服务,但仍然允许测试流式RPC。

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

这种方法的好处是,你仍然可以获得网络行为,但是通过内存连接而不使用像端口这样的操作系统级资源,这些资源可能清理得快也可能清理得慢。它允许你按照实际使用的方式进行测试,并提供适当的流式行为。

我脑海中没有一个流式示例,但上面的代码就是关键。它提供了与正常网络连接的所有预期行为。关键是设置了如上所示的WithDialer选项,使用bufconn包创建一个公开自己的拨号器的监听器。我经常使用这种技术来测试gRPC服务,效果很好。

英文:

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.

答案2

得分: 58

如果您想验证gRPC服务的实现是否符合预期,那么您可以编写标准的单元测试,并完全忽略网络部分。

例如,创建greeter_server_test.go文件:

func HelloTest(t *testing.T) {
    s := server{}
    
    // 设置测试用例
    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) 发生意外错误", tt.name)
        }
        if resp.Message != tt.want {
            t.Errorf("HelloText(%v)=%v,期望值为 %v", tt.name, resp.Message, tt.want)
        }
    }
}

我可能在从记忆中编写proto语法时弄错了一些地方,但这就是基本思路。

英文:

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.

答案3

得分: 22

这里可能有一种更简单的方法来测试流媒体服务。如果有任何拼写错误,请原谅,因为我是根据一些正在运行的代码进行调整的。

给定以下定义。

rpc ListSites(Filter) returns(stream sites) 

使用以下服务器端代码。

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

现在,你只需要在测试文件中模拟 pb.SitesService_ListSitesServer

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
}

这个模拟对象会响应 .send 事件,并将发送的对象记录在 .Results 中,然后你可以在断言语句中使用它们。

最后,使用模拟的 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")
}

这并不测试整个堆栈,但它允许你在不运行完整的 gRPC 服务(无论是真实的还是模拟的)的情况下,对服务器端代码进行合理性检查。

英文:

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.

答案4

得分: 16

有很多方法可以选择来测试 gRPC 服务。根据你想要达到的信心水平,你可以选择不同的测试方式。以下是三种常见场景的示例。

场景一:我想测试我的业务逻辑

在这种情况下,你对服务中的逻辑以及它与其他组件的交互感兴趣。在这里,最好的做法是编写一些单元测试。

Alex Ellis 写了一篇关于在 Go 中进行单元测试的很好的介绍文章。如果你需要测试交互,那么 GoMock 是一个不错的选择。Sergey Grebenshchikov 写了一个很好的 GoMock 教程

Omar 的回答展示了如何对这个特定的 SayHello 示例进行单元测试。

场景二:我想通过网络手动测试我的实时服务的 API

在这种情况下,你对手动探索性地测试你的 API 感兴趣。通常这是为了探索实现、检查边界情况并确保你的 API 表现符合预期。

你需要:

  1. 启动你的 gRPC 服务器
  2. 使用一个通过网络的模拟解决方案来模拟你可能有的任何依赖关系,例如,如果你的被测试的 gRPC 服务调用了另一个服务。例如,你可以使用 Traffic Parrot
  3. 使用一个 gRPC API 测试工具,例如 gRPC CLI

现在,你可以使用模拟解决方案来模拟真实和假设的情况,同时使用 API 测试工具观察被测试服务的行为。

场景三:我想要对我的 API 进行自动化的网络测试

在这种情况下,你对编写自动化的 BDD 风格的验收测试感兴趣,这些测试通过网络的 gRPC API 与被测试系统进行交互。这些测试编写、运行和维护成本较高,应该谨慎使用,同时要记住测试金字塔

thinkerou 的回答展示了如何使用 karate-grpc 在 Java 中编写这些 API 测试。你可以结合 Traffic Parrot Maven 插件 来模拟任何通过网络的依赖关系。

英文:

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.

答案5

得分: 15

我想到了以下的实现方式,可能不是最佳的方法。主要是使用TestMain函数来启动服务器,使用goroutine这样的方式:

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

然后在其他测试中实现客户端:

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

    })
}
英文:

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

	})
}

答案6

得分: 9

顺便说一下,作为一个新的贡献者,我不能添加评论。所以我在这里添加一个新的回答。

我可以确认,@Omar的方法可以用于测试非流式的gRPC服务,通过在没有运行服务的情况下通过接口进行测试。

然而,这种方法对于流式传输是行不通的。由于gRPC支持双向流,必须启动服务并通过网络层连接到它才能进行流式传输的测试。

@joscas采用的方法适用于gRPC流(尽管helloworld示例代码不使用流),它使用goroutine启动服务。然而,我注意到在Mac OS X 10.11.6上,当从goroutine中调用时,它不会始终释放服务使用的端口(据我了解,服务将阻塞goroutine,可能无法正常退出)。通过使用"exec.Command"在单独的进程中启动服务运行,并在完成之前将其终止,可以始终释放端口。

我在github上上传了一个使用流式传输的gRPC服务的工作测试文件:https://github.com/mmcc007/go/blob/master/examples/route_guide/server/server_test.go

您可以在travis上看到测试运行的情况:https://travis-ci.org/mmcc007/go

如果对如何改进gRPC服务的测试有任何建议,请告诉我。

英文:

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.

答案7

得分: 3

作为新的贡献者,我无法发表评论,所以我在这里作为答案添加。

@shiblon的答案是测试您的服务的最佳方法。我是*grpc-for-production*的维护者之一,其中一个功能是一个在处理中的服务器,使得使用bufconn更容易。

这里是一个测试greeter服务的示例:

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将使用内存数据传输而不是正常的网络传输来测试HelloWorld服务
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")
}

您可以在这里找到此示例。

英文:

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

答案8

得分: -1

你可以使用karate-grpc来测试gRPC服务,只需要提供你的proto jar文件和gRPC服务器的IP/端口。karate-grpc是基于karate和polyglot构建的。

下面是一个hello-world的示例:

功能:通过gRPC动态客户端的gRPC helloworld示例

背景:

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

场景:执行操作

  • 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'

关于karate-grpc示例的注释:

测试一个 gRPC 服务

它将生成漂亮的报告,如下所示:

测试一个 gRPC 服务

更多详细信息请参见:https://thinkerou.com/karate-grpc/

英文:

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:

测试一个 gRPC 服务

And it will generate beautiful report, like:

测试一个 gRPC 服务

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

huangapple
  • 本文由 发表于 2017年2月8日 08:22:29
  • 转载请务必保留本文链接:https://go.coder-hub.com/42102496.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定