开发任何软件应用程序(包括 gRPC 应用程序)都要有和应用程序相关联的单元测试。gRPC 应用程序始终会与网络交互,测试应该涵盖服务器端和客户端 gRPC 应用程序的网络方面。我们首先测试 gRPC 服务器端。
7.1.1 测试gRPC服务器端
gRPC 服务的测试通常使用 gRPC 客户端应用程序来完成,该客户端应用程序是测试用例的一部分。服务器端的测试包括使用所需的服务启动gRPC 服务器,并使用实现测试用例的客户端应用程序连接到服务器。
我们看一个使用 Go 语言编写的测试用例,它对 ProductInfo 服务进行了测试。在 Go 语言中,gRPC 测试用例应该是使用 testing 包的 Go通用测试用例来实现的(见代码清单 7-1)。
代码清单 7-1 使用 Go 语言编写的 gRPC 服务器端测试
func TestServer_AddProduct(t *testing.T) { ➊
grpcServer := initGRPCServerHTTP2() ➋
conn, err := grpc.Dial(address, grpc.WithInsecure()) ➌
if err != nil {
grpcServer.Stop()
t.Fatalf("did not connect: %v", err)
}
defer conn.Close()
c := pb.NewProductInfoClient(conn)
name := "Sumsung S10"
description := "Samsung Galaxy S10 is the latest smart phone, launched in February 2019"
price := float32(700.0)
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
r, err := c.AddProduct(ctx, &pb.Product{Name: name,Description: description, Price: price}) ➍
if err != nil { ➎
t.Fatalf("Could not add product: %v", err)
}
if r.Value == "" {
t.Errorf("Invalid Product ID %s", r.Value)
}
log.Printf("Res %s", r.Value)
grpcServer.Stop()
}
- ❶ 常规测试,启动 gRPC 服务器和客户端以使用 RPC 测试服务。
- ❷ 在 HTTP/2 之上启动常规的 gRPC 服务器。
- ❸ 连接服务器端应用程序。
- ❹ 向 AddProduct 方法发送 RPC。
- ❺ 校验响应消息。
因为 gRPC 测试用例是基于语言的标准测试用例,所以执行它们的方式与标准测试用例没什么不同。服务器端测试用例有一个特殊的地方,那就是它们需要服务器端应用程序开启一个供客户端应用程序连接的端口。如果你不喜欢这样做,或者测试环境不允许这样做,那么可以使用某个库来避免在真正的端口上启用服务。在 Go 语言中,可以使用bufconn 包,它提供了 net.Conn,这是通过缓冲区和相关的 dial 与监听功能实现的。你可以在本章的源代码仓库中找到完整的代码示例。如果使用的是 Java,那么可以使用像 JUnit 这样的测试框架,并遵循完全相同的过程来编写服务器端的 gRPC 测试。但是,如果想在编写测试用例时不启动 gRPC 服务器实例,那么可以使用 Java 实现的 gRPC 进程内服务器端。
在本书的源代码仓库中,可以找到完整的 Java 代码示例。
我们还可以编写远程函数业务逻辑的单元测试,避免涉及 RPC 网络层。可以通过调用函数来直接对它们进行测试,而无须使用 gRPC 客户端。
到此为止,我们已经学习了如何为 gRPC 服务编写测试。接下来看一下如何测试 gRPC 客户端。
7.1.2 测试gRPC客户端
当为 gRPC 客户端开发测试时,有种可行的测试方式就是启动一台gRPC 服务器并实现 mock 服务。但是,这并不是一个简单的任务,因为这会有打开端口和连接服务器端的开销。因此,想要测试客户端的逻辑却不想要连接真正的服务器端所带来的开销,可以使用 mock 框架。
对 gRPC 服务器端进行 mock,能够让开发人员在客户端编写轻量级单元测试,来对功能进行检查,避免对服务器进行 RPC。
如果使用 Go 语言开发 gRPC 客户端应用程序,则可以(借助生成的代码)使用 Gomock 来模拟客户端接口,并通过编码的方式设置方法以接收和返回预先确定的值。在使用 Gomock 时,可以通过如下命令为gRPC 客户端应用程序生成 mock 接口:
mockgen github.com/grpc-up-and-running/samples/ch07/grpc-docker/go/proto-gen \
ProductInfoClient > mock_prodinfo/prodinfo_mock.go
这里指定 ProductInfoClient 是要模拟的接口。然后,所编写的测试代码可以导入 mockgen 生成的包以及 gomock 包,从而为客户端逻辑编写单元测试。如代码清单 7-2 所示,可以创建一个 mock 对象,预期对它的方法进行调用并返回一个响应。
代码清单 7-2 使用 Gomock 进行 gRPC 客户端测试
func TestAddProduct(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mocklProdInfoClient := NewMockProductInfoClient(ctrl) ➊
...
req := &pb.Product{Name: name, Description: description, Price: price}
mocklProdInfoClient. ➋
EXPECT().AddProduct(gomock.Any(), &rpcMsg{msg: req},). ➌
Return(&wrapper.StringValue{Value: "ABC123" + name}, nil) ➍
testAddProduct(t, mocklProdInfoClient) ➎
}
func testAddProduct(t *testing.T, client pb.ProductInfoClient) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
...
r, err := client.AddProduct(ctx, &pb.Product{Name: name,Description: description, Price: price})
// 测试并校验响应
}
- ❶ 创建 mock 对象,预期对远程方法进行调用。
- ❷ 对 mock 对象进行编码。
- ❸ 预期调用 AddProduct 方法。
- ❹ 返回商品 ID 的 mock 值。
- ❺ 调用实际的测试方法,它会调用客户端存根的远程方法。
如果你使用的是 Java,则可以使用 Mockito 和针对 Java gRPC 的进程内服务器实现来测试客户端应用程序。你可以参考源代码仓库来了解示例详情。服务器端和客户端所需的测试准备好后,就可以将它们集成到所使用的持续集成工具中了。
需要记住的要点是,mock gRPC 服务器不会带来与真实的 gRPC 服务器端完全相同的行为。因此,特定的功能可能无法通过测试来校验,除非重新实现 gRPC 服务器端可能出现的所有错误逻辑。在实践中,可以通过 mock 校验一组选定的功能,而其他的功能则需要通过真正的 gRPC服务器实现来验证。下面来看一下如何对 gRPC 应用程序进行负载测试和基准测量。
7.1.3 负载测试
使用常规的工具很难对 gRPC 应用程序进行负载测试和基准测量,这是因为这些应用程序都或多或少是与特定协议(如 HTTP)绑定的。对于gRPC 来说,我们需要定制的负载测试工具,这些工具能够生成对服务器端的虚拟 RPC 负载,从而实现对 gRPC 服务器端的负载测试。
ghz 就是这样的负载测试工具,它是使用 Go 语言实现的命令行工具。
它能够在本地对服务进行测试和调试,也能用在自动化持续集成环境中,实现性能回归测试。例如,可以通过如下命令利用 ghz 执行负载测试:
ghz --insecure \
--proto ./greeter.proto \
--call helloworld.Greeter.SayHello \
-d '{"name":"Joe"}'\
-n 2000 \
-c 20 \
0.0.0.0:50051
这里以非安全的方式调用 Greeter 服务的 SayHello 远程方法。可以设置总的请求数(-n 2000
)和并发数(20 个线程)。测试结果能够以各种输出格式生成。
服务器端和客户端所需的测试准备就绪之后,就可以将它们集成到所使用的持续集成工具中了。
7.1.4 持续集成
如果你刚接触持续集成(continuous integration,CI),那么可以将其描述为一种需要开发人员频繁地将代码集成到一个共享仓库的开发实践。
每次提交的代码都会通过自动构建进行验证,这样能够让团队尽早地发现问题。就 gRPC 应用程序来讲,通常服务器端应用程序和客户端应用程序相互独立,它们可能是使用完全不同的技术构建的。因此,作为CI 过程的一部分,必须使用 7.1.3 节介绍的单元测试和集成测试技术,来验证 gRPC 客户端和服务器端的代码。然后,基于所使用的语言来构建 gRPC 应用程序。可以用所选择的 CI 工具来集成这些应用程序的测试(如 Go testing 或 Java JUnit)。例如,你使用 Go 编写测试,那么就可以很容易地将 Go 测试与 Jenkins、TravisCI 和 Spinnaker 这样的工具集成。
为 gRPC 应用程序完成搭建测试和 CI 过程后,接下来要学习的就是gRPC 应用程序的部署。