测试 gRPC 服务

Posted

技术标签:

【中文标题】测试 gRPC 服务【英文标题】:Testing a gRPC service 【发布时间】:2017-06-25 10:38:57 【问题描述】:

我想测试一个用 Go 编写的 gRPC 服务。我使用的示例是来自 grpc-go repo 的 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 main 中的类型是:

// 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.HelloReplyMessage: "Hello " + in.Name, nil

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

【问题讨论】:

作为旁注:注意默认的 4MiB 限制 对于 gRPC,我通常使用grpc.techunits.com 和 sConnector 作为我的接口。 sConnector 还没有完全功能化,我认为很好开始。 【参考方案1】:

我认为您正在寻找 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 服务,效果很好。

【讨论】:

请注意,这个包在提问时不可用。这就是@omar 的回答最初被接受的原因。 如果您需要测试 GRPC 错误处理、错误包装和返回状态是否按预期工作,此方法特别有用。【参考方案2】:

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

例如,使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.HelloRequestName: 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)
        
    

我可能在记忆中弄乱了 proto 语法,但这就是我的想法。

【讨论】:

我不认为这对测试 rpc 代码有好处。我们应该让 rpc 客户端和 rpc 服务器的代码都运行,以验证端到端的请求和响应是否正常。 @LewisChan 这对于运行业务逻辑很有用 有时我们需要使用拦截器来测试服务行为【参考方案3】:

这可能是一种更简单的测试流媒体服务的方法。如果有任何拼写错误,我深表歉意,因为我正在从一些正在运行的代码中调整它。

给定以下定义。

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 服务。

【讨论】:

【参考方案4】:

我想出了以下实现,这可能不是最好的方法。主要是使用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.HelloRequestName: 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)
        

    )

【讨论】:

顺便说一下,除了定义portaddress 变量之外,您还可以将端口留空,例如net.Listen("tcp", ":"),并使用lis.Addr().String() 来获取自动选择的地址(参见@ 987654321@)。这可以防止测试失败,因为该地址已在使用中。【参考方案5】:

您可以选择多种方法来测试 gRPC 服务。您可以选择以不同方式进行测试,具体取决于您希望获得的信心。以下是说明一些常见场景的三个案例。

案例#1:我想测试我的业务逻辑

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

Alex Ellis 有一个很好的introduction to unit testing in Go。 如果您需要测试交互,那么GoMock 是您的最佳选择。 Sergey Grebenshchikov 写了一个很好的GoMock tutorial。

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

案例 #2:我想通过网络手动测试我的实时服务的 API

在这种情况下,您有兴趣手动对您的 API 进行探索性测试。通常这样做是为了探索实现、检查边缘情况并确信您的 API 行为符合预期。

您需要:

    开始你的gRPC server 使用在线模拟解决方案模拟您拥有的任何依赖项,例如如果您正在测试的 gRPC 服务对另一个服务进行 gRPC 调用。例如,您可以使用Traffic Parrot。 使用 gRPC API 测试工具。例如,您可以使用gRPC CLI。

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

案例 #3:我希望通过网络对我的 API 进行自动化测试

在这种情况下,您有兴趣编写自动化的 BDD 样式验收测试,这些测试通过在线 gRPC API 与被测系统交互。这些测试的编写、运行和维护成本很高,应谨慎使用,请记住testing pyramid。

answer from thinkerou 展示了如何使用karate-grpc 在 Java 中编写这些 API 测试。您可以将其与 Traffic Parrot Maven plugin 结合使用,以模拟任何在线依赖项。

【讨论】:

【参考方案6】:

顺便说一句:作为一个新的贡献者,我不能添加到 cmets。所以我在这里添加一个新的答案。

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

但是,这种方法不适用于流。由于 gRPC 支持双向流,因此需要启动服务并通过网络层连接到它来进行流测试。

@joscas 采用的方法适用于使用 goroutine 启动服务的 gRPC 流(即使 helloworld 示例代码不使用流)。但是,我注意到在 Mac OS X 10.11.6 上,当从 goroutine 调用时,它不会一致地释放服务使用的端口(据我了解,该服务将阻塞 goroutine 并且可能不会干净地退出)。通过启动一个单独的进程让服务在其中运行,使用“exec.Command”,并在完成之前将其终止,端口被一致地释放。

我将使用流的 gRPC 服务的工作测试文件上传到 github:https://github.com/mmcc007/go/blob/master/examples/route_guide/server/server_test.go

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

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

【讨论】:

【参考方案7】:

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

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

这里是一个测试欢迎服务的例子

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.HelloRequestName: "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")

你可以找到这个例子here

【讨论】:

【参考方案8】:

您可以使用karate-grpc 来测试grpc 服务,您只需要发布您的proto jar 和grpc 服务器ip/port。 karate-grpc 基于空手道和多语言构建。

一个 hello-world 示例:

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'

关于 karate-grpc 注释示例:

它会生成漂亮的报告,比如:

更多详情请看:https://thinkerou.com/karate-grpc/

【讨论】:

以上是关于测试 gRPC 服务的主要内容,如果未能解决你的问题,请参考以下文章

gRPC服务开发和接口测试初探Go

在使用 Springboot 运行集成测试时启动嵌入式 gRPC 服务器

是否可以使用 ngrok 或 localtunnel 来测试本地 gRPC 服务器?

Locust完成gRPC协议的性能测试

Locust完成gRPC协议的性能测试

gRPC三种Java客户端性能测试实践