测试gRPC服务

sub*_*ing 31 go grpc

我想测试用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;
}
Run Code Online (Sandbox Code Playgroud)

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
}
Run Code Online (Sandbox Code Playgroud)

我找了一些例子但是我找不到如何在Go中实现gRPC服务的测试.

小智 37

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

例如,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)
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

我可能已经从内存中弄乱了原始语法,但这就是想法.

  • 我不认为这对于测试 rpc 代码有好处。我们应该让rpc客户端和rpc服务器的代码全部运行来验证端到端的请求和响应是否正常。 (2认同)

shi*_*lon 29

我想你正在寻找"google.com/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(string, time.Duration) (net.Conn, error) {
    return lis.Dial()
}

func TestSayHello(t *testing.T) {
    ctx := context.Background()
    conn, err := grpc.DialContext(ctx, "bufnet", grpc.WithDialer(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.
}
Run Code Online (Sandbox Code Playgroud)

这种方法的好处是你仍然可以获得网络行为,但是通过内存连接而不使用操作系统级资源,例如可能会或可能不会快速清理的端口.它允许您以实际使用的方式对其进行测试,并为您提供正确的流式传输行为.

我没有一个流动的例子,但是神奇的酱汁就在上面.它为您提供了正常网络连接的所有预期行为.诀窍是设置WithDialer选项,如图所示,使用bufconn包创建一个暴露自己的拨号器的监听器.我一直使用这种技术来测试gRPC服务,它运行良好.

  • 请注意,在提出问题时此软件包不可用。这就是为什么 @omar 的答案最初被接受的原因。 (3认同)

sub*_*ing 10

我想出了以下实施方式,但这可能不是最好的方法。主要使用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())
}
Run Code Online (Sandbox Code Playgroud)

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

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

    })
}
Run Code Online (Sandbox Code Playgroud)

  • 顺便说一句,除了定义 `port` 和 `address` 变量,您也可以将端口留空,例如 `net.Listen("tcp", ":")`,并使用 `lis.Addr().String() ` 获取自动选择的地址(参见 https://godoc.org/net#Listen)。这可以防止测试失败,因为该地址已被使用。 (3认同)

小智 10

这可能是一种仅测试流服务的简单方法.如果有任何拼写错误,我会道歉,因为我正在从一些正在运行的代码中调整它.

给出以下定义.

rpc ListSites(Filter) returns(stream sites) 
Run Code Online (Sandbox Code Playgroud)

使用以下服务器端代码.

// 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
}
Run Code Online (Sandbox Code Playgroud)

现在您要做的就是在测试文件中模拟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
}
Run Code Online (Sandbox Code Playgroud)

这会响应.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")
}
Run Code Online (Sandbox Code Playgroud)

不,它不会测试整个堆栈,但它确实允许您理智地检查您的服务器端代码,而无需为真实或模拟形式运行完整的gRPC服务.


mmc*_*abe 7

顺便说一句:作为新的贡献者,我无法添加评论。所以我在这里添加一个新答案。

我可以通过不运行服务的接口进行测试来确认@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服务测试的任何建议。


Lia*_*ams 6

您可以选择多种方式来测试gRPC服务。您可以选择不同的测试方式,具体取决于您希望获得的信任程度。以下三种情况说明了一些常见情况。

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

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

Alex Ellis 在Go中很好地介绍了单元测试。如果您需要测试交互,那么GoMock是您的最佳选择。Sergey Grebenshchikov写了一个不错的GoMock教程

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

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

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

您将需要:

  1. 启动您的gRPC服务器
  2. 使用在线模拟解决方案模拟您具有的任何依赖关系,例如,如果您的被测试gRPC服务对另一个服务进行了gRPC调用。例如,您可以使用Traffic Parrot
  3. 使用gRPC API测试工具。例如,您可以使用gRPC CLI

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

案例3:我想自动完成API的线路测试

在这种情况下,您有兴趣编写自动化的BDD样式验收测试,该测试通过在线gRPC API与被测系统进行交互。这些测试的编写,运行和维护都很昂贵,并且应牢记测试金字塔,并应谨慎使用。

thinkerou答案显示了如何使用karate-grpc来用Java编写这些API测试。您可以将其与Traffic Parrot Maven插件结合使用,以模拟任何在线依赖关系。