gRPC C++ 客户端单元测试实战:基于 gmock Mock Stub 编写无网络 RPC 的同步 API 测试 gRPC C 客户端单元测试实战基于 gmock Mock Stub 编写无网络 RPC 的同步 API 测试【免费下载链接】grpcC based gRPC (C, Python, Ruby, Objective-C, PHP, C#)项目地址: https://gitcode.com/GitHub_Trending/gr/grpc本文对应仓库文档 doc/unit_testing.md介绍在 gRPC C 同步SynchronousAPI 下如何对客户端业务逻辑进行单元测试。核心思路是利用代码生成器产出基于 gmock 的 Mock Stub在不真正建立连接、不发任何 RPC 的前提下通过设置期望值expectations驱动被测客户端代码的执行与断言。读完本文你将掌握Mock Stub 的生成原理protoc 插件 flag 与 Bazel 属性两条路径、StubInterface/*Raw方法结构、Unary 与四种 Streaming RPC 形态的 mock 测试写法以及仓库 test/cpp/end2end/mock_test.cc 中先跑真实 RPC 再切 Mock的完整对照示例。一、为什么要为客户端 Mock Stub在典型的分层测试里服务端 handler 可以借助 gRPC 自带的测试 Service、内存 channelin-process server做端到端验证但客户端侧如何组包、如何调用 Stub、如何解析返回值与错误状态这类逻辑如果每次都起真实 server 再走一遍网络栈测试会变慢且受环境干扰。因此 gRPC C 为同步 API 提供了一条更轻量的单元测试路径基于 googletest / googlemock 生成一个可编程的 Mock Stub测试代码只需对它设置调用期望次数、参数、返回值被测客户端便可以在零 RPC的情况下被驱动起来从而隔离验证纯客户端逻辑。这一点正是 doc/unit_testing.md 的主题test client-side logic without having to make any rpcs。二、从 proto 到可被 mock 的生成代码结构2.1 一个包含单播与双端流的服务定义文档以EchoTestService为例该服务在仓库中真实存在见 src/proto/grpc/testing/echo.proto其中还包含客户端流、服务端流等多种形态service EchoTestService { rpc Echo(EchoRequest) returns (EchoResponse); rpc BidiStream(stream EchoRequest) returns (stream EchoResponse); }2.2StubInterfaceMock 的真正落点经grpc_cpp_plugin生成的头文件中每个服务会得到一个StubInterface纯虚接口同步调用的客户端公开方法与流工厂方法都以它为基础。生成结果形如下面由 doc/unit_testing.md 概括的结构class EchoTestService final { public: class StubInterface { // Unary RPC纯虚方法可直接被 mock virtual ::grpc::Status Echo(::grpc::ClientContext* context, const ::grpc::testing::EchoRequest request, ::grpc::testing::EchoResponse* response) 0; // ... // Streaming RPC对外提供的是非虚的“包装”方法 BidiStream(...) // 它内部调用一个私有纯虚方法 BidiStreamRaw(...) std::unique_ptr::grpc::ClientReaderWriterInterface ::grpc::testing::EchoRequest, ::grpc::testing::EchoResponse BidiStream(::grpc::ClientContext* context) { return std::unique_ptr::grpc::ClientReaderWriterInterface ::grpc::testing::EchoRequest, ::grpc::testing::EchoResponse( BidiStreamRaw(context)); } // ... private: // Streaming 的“真身”返回裸指针是 mock 需要拦截的纯虚方法 virtual ::grpc::ClientReaderWriterInterface ::grpc::testing::EchoRequest, ::grpc::testing::EchoResponse* BidiStreamRaw(::grpc::ClientContext* context) 0; // ... }; // End StubInterface }; // End EchoTestService这段结构有两个对测试至关重要的点Unary 方法如Echo本身就是纯虚gmock 可直接覆盖它。流式方法如BidiStream是非虚包装 私有纯虚*Raw的组合。*Raw返回裸指针交给智能指针包装。所以写 mock 时要针对BidiStreamRaw、RequestStreamRaw、ResponseStreamRaw这类*Raw方法设置期望而不是针对对外包装方法本身。在仓库 test/cpp/end2end/mock_test.cc 中可印证这一点测试里对 stub 设置的期望分别是Echo(_, _, _)、RequestStreamRaw(_, _)、ResponseStreamRaw(_, _)、BidiStreamRaw(_)见该文件的SimpleRpc、ClientStream、ServerStream、BidiStream四个用例。2.3 手写一个 Mock Stub 长什么样Mock 只需要继承StubInterface并用MOCK_METHOD*覆盖其纯虚方法。对文档中的示例而言class MockEchoTestServiceStub : public EchoTestService::StubInterface { public: MOCK_METHOD3(Echo, ::grpc::Status(::grpc::ClientContext* context, const ::grpc::testing::EchoRequest request, ::grpc::testing::EchoResponse* response)); MOCK_METHOD1(BidiStreamRaw, ::grpc::ClientReaderWriterInterface ::grpc::testing::EchoRequest, ::grpc::testing::EchoResponse*( ::grpc::ClientContext* context)); };流式 RPC 返回的ClientReaderWriterInterface/ClientReaderInterface/ClientWriterInterface同样有对应的现成 Mock 类可直接使用见后文第四节。三、自动生成 Mock 代码的两条路径手写每个服务的 Mock 既繁琐又容易随 proto 变更而失步因此 gRPC 官方同时提供了两条自动生成路径生成的产物一致一个名为服务名_mock.grpc.pb.h的头文件格式见 bazel/generate_cc.bzl 中的_GRPC_PROTO_MOCK_HEADER_FMT {}_mock.grpc.pb.h。3.1 路径一protoc grpc_cpp_plugin 的代码生成 flag在执行protoc时为 gRPC C 插件设置generate_mock_codetrue选项即可命令形式为protoc -I . \ --grpc_outgenerate_mock_codetrue:. \ --pluginprotoc-gen-grpcwhich grpc_cpp_plugin \ echo.proto执行后除了常规的echo.grpc.pb.h/echo.grpc.pb.cc之外还会额外生成一个包含 Mock Stub 的头文件echo_mock.grpc.pb.h。该 flag 对应的参数generate_mock_code定义在插件接口 src/compiler/cpp_generator.h 的代码生成参数结构中真正输出 mock 文件时生成器还会在文件头按需引入 gmock 头文件当未显式指定gmock_search_path时默认 includegmock/gmock.h对应实现见 src/compiler/cpp_generator.cc 中 mock 文件的 includes 处理逻辑。3.2 路径二Bazel 规则属性generate_mocks在 Bazel 构建系统中则是在 proto 库规则上声明generate_mocks Truegrpc_proto_library( name echo_proto, srcs [echo.proto], generate_mocks True, )仓库内部的规则体系对此做了完整串联generate_mocks作为布尔属性存在于cc_grpc_library中默认值为False见 bazel/cc_grpc_library.bzl其属性说明指出当为 True 时为客户端 Stub 生成 Google Mock 代码bazel/cc_grpc_library.bzl并最终透传给底层生成规则bazel/cc_grpc_library.bzlgrpc_proto_library/grpc_library等公共包装同样把该开关一路下传见 bazel/grpc_build_system.bzl最终由 bazel/generate_cc.bzl 判断是否执行 mock 生成并产出_mock.grpc.pb.h。3.3 生成产物如何使用Mock 头文件可与echo.grpc.pb.h含StubInterface、消息类型一同 include 进测试文件同时需要引入 gmock测试代码需链接 gmock/gtest 依赖。仓库中的权威示例即如此mock_test.cc顶部 include 了src/proto/grpc/testing/echo_mock.grpc.pb.h自动生成的 Mock Stub、grpcpp/test/mock_stream.h流式 mock 类以及gmock/gmock.h/gtest/gtest.h并直接使用了生成的grpc::testing::MockEchoTestServiceStub类型。四、用 Mock Stub 编写客户端测试依赖注入 期望设置4.1 被测客户端把 Stub 接口注入进来为了让客户端既可接真实 Stub、又可接 Mock Stub应让客户端面向StubInterface编程并通过构造函数或 setter 注入依赖。文档中的FakeClient正是这种模式仓库中 test/cpp/end2end/mock_test.cc 有结构一致的完整实现且覆盖了四种 RPC 形态class FakeClient { public: explicit FakeClient(EchoTestService::StubInterface* stub) : stub_(stub) {} void DoEcho() { ClientContext context; EchoRequest request; EchoResponse response; request.set_message(hello world); Status s stub_-Echo(context, request, response); EXPECT_EQ(request.message(), response.message()); EXPECT_TRUE(s.ok()); } void DoBidiStream() { EchoRequest request; EchoResponse response; ClientContext context; std::string msg(hello); std::unique_ptrClientReaderWriterInterfaceEchoRequest, EchoResponse stream stub_-BidiStream(context); request.set_message(msg 0); EXPECT_TRUE(stream-Write(request)); EXPECT_TRUE(stream-Read(response)); EXPECT_EQ(response.message(), request.message()); request.set_message(msg 1); EXPECT_TRUE(stream-Write(request)); EXPECT_TRUE(stream-Read(response)); EXPECT_EQ(response.message(), request.message()); request.set_message(msg 2); EXPECT_TRUE(stream-Write(request)); EXPECT_TRUE(stream-Read(response)); EXPECT_EQ(response.message(), request.message()); stream-WritesDone(); EXPECT_FALSE(stream-Read(response)); Status s stream-Finish(); EXPECT_TRUE(s.ok()); } void ResetStub(EchoTestService::StubInterface* stub) { stub_ stub; } private: EchoTestService::StubInterface* stub_; };说明原文档示例代码中的msg 0为文档排版笔误此处按仓库源码统一写成字符串拼接msg 0参见 test/cpp/end2end/mock_test.cc。这种注入 可切换ResetStub的设计使得同一个被测对象既能对着真实 channel 跑也能瞬间切换到 mock是验证 mock 正确性的关键手法。4.2 Unary RPC 的 mock 测试写法Unary 方法的 Mock 测试要点对Echo(context, request, response)设置Times期望调用次数与WillOnce首次行为并通过SetArgPointee2写入出参response、Return(Status::OK)指定返回值MockEchoTestServiceStub stub; EchoResponse resp; resp.set_message(hello world); EXPECT_CALL(stub, Echo(_,_,_)) .Times(AtLeast(1)) .WillOnce(DoAll(SetArgPointee2(resp), Return(Status::OK))); FakeClient client(stub); // 以 mock 初始化 client.DoEcho(); // 全程不发生任何真实 RPC对应到仓库测试 test/cpp/end2end/mock_test.cc该用例先让FakeClient对着真实 server 跑一遍DoEcho()再通过ResetStub(stub)换入 mock 重跑一遍代码注释也写明这是Do one real rpc and one mocked one——先用真实链路证明被测逻辑本身没问题再用 mock 证明测试在不发 RPC 时同样能驱动并断言同一段逻辑。4.3 流式 RPC配合mock_stream.h的现成 Mock 流类流式 RPC 的 mock 需要两件事同时就位对 stub 的*Raw方法设置期望让它返回一个假的流对象该假的流对象本身也必须是 mock能对Write/Read/WritesDone/Finish等调用做出脚本化响应。gRPC 已在 include/grpcpp/test/mock_stream.h 中提供三个现成模板类它们分别继承grpc::ClientReaderInterface/grpc::ClientWriterInterface/grpc::ClientReaderWriterInterface并对关键方法做了MOCK_METHOD化MockClientReaderRmockRead(R*)、NextMessageSize(uint32_t*)、WaitForInitialMetadata()、Finish()include/grpcpp/test/mock_stream.hMockClientWriterWmockWrite(const W, const WriteOptions)、WritesDone()、Finish()include/grpcpp/test/mock_stream.hMockClientReaderWriterW, R同时 mockRead/Write/NextMessageSize/WaitForInitialMetadata/WritesDone/Finishinclude/grpcpp/test/mock_stream.h。双端流BidiStream的完整 mock 写法如下来自 doc/unit_testing.md与仓库 test/cpp/end2end/mock_test.cc 的BidiStream用例一致// 自定义 gmock action把内部保存的消息拷贝到 Read 的出参中 ACTION_P(copy, msg) { arg0-set_message(msg-message()); } auto rw new MockClientReaderWriterEchoRequest, EchoResponse(); EchoRequest msg; // 客户端会连续 Write 3 次把每次写入的消息暂存起来SaveArg EXPECT_CALL(*rw, Write(_, _)) .Times(3) .WillRepeatedly(DoAll(SaveArg0(msg), Return(true))); // 前 3 次 Read 都回显刚写入的消息第 4 次返回 false 表示流结束 EXPECT_CALL(*rw, Read(_)) .WillOnce(DoAll(WithArg0(copy(msg)), Return(true))) .WillOnce(DoAll(WithArg0(copy(msg)), Return(true))) .WillOnce(DoAll(WithArg0(copy(msg)), Return(true))) .WillOnce(Return(false)); MockEchoTestServiceStub stub; EXPECT_CALL(stub, BidiStreamRaw(_)) .Times(AtLeast(1)) .WillOnce(Return(rw)); FakeClient client(stub); client.DoBidiStream();这里的调用链完美呼应了 2.2 节的生成结构FakeClient::DoBidiStream()调用stub_-BidiStream(context)非虚包装方法→ 内部触发纯虚BidiStreamRaw(context)→ 被 mock 拦下并返回rw一个MockClientReaderWriter随后流上的每一次Write/Read都由rw的期望脚本化应答全程没有任何网络字节流产生。4.4 其余流式形态ClientStream / ServerStream仓库的 test/cpp/end2end/mock_test.cc 还把同一套方法论推广到了另外两种流式形态对应 src/proto/grpc/testing/echo.proto 中声明的RequestStream、ResponseStream方法客户端流RequestStream用例ClientStream见 test/cpp/end2end/mock_test.cc用new MockClientWriterEchoRequest()充当假流对Write(_, _)期望 2 次并返回true对WritesDone()/Finish()设定期望同时对 stub 的RequestStreamRaw(_, _)使用SetArgPointee1(resp)预设最终聚合出的响应服务端流ResponseStream用例ServerStream见 test/cpp/end2end/mock_test.cc用new MockClientReaderEchoResponse()充当假流通过级联WillOnce让Read(_)依次返回两条不同消息再返回false最后Finish()返回Status::OK。由此Stub 纯虚方法 *Raw返回裸流指针 mock_stream.h现成流 mock这一套组合可以覆盖 gRPC 全部四种 RPC 语义unary、client streaming、server streaming、bidi streaming。五、仓库实测样本中的更多细节5.1 先真后 Mock 的对照组测试思路MockTest测试夹具见 test/cpp/end2end/mock_test.cc在SetUp()中会通过ServerBuilderInsecureServerCredentials在本地起一个真实的TestServiceImpl并在ResetStub()里用CreateCustomChannel创建真实 Stub。四个测试用例都遵循同一模式ResetStub()→ 用真实 Stub 跑一遍FakeClient的业务方法验证被测逻辑本身正确构造 Mock Stub 与流 mock设置期望client.ResetStub(stub)→ 在不连接任何 server的情况下重跑同一业务方法靠 mock 期望驱动全部交互并完成断言。这种写法把被测逻辑有 bug和mock 脚本写错两类问题区分开是值得借鉴的测试结构。5.2 服务端 handler 也可脱离真实传输做单测仓库还在同文件中示范了对同步 Service 服务端 handler的单元测试替代方案借助CallbackTestServiceImpl与 include/grpcpp/test/default_reactor_test_peer.h 的DefaultReactorTestPeer可以直接驱动CallbackService的 reactor 完成回调、检查完成状态码MockedCallSucceeds、MockedCallFails等用例见 test/cpp/end2end/mock_test.cc。这说明在 gRPC C 中服务端与客户端的核心逻辑都具备不经过真实传输的单元测试路径。5.3 如何构建与运行这些测试该测试文件已被登记在 Bazel 构建描述 test/cpp/end2end/BUILD 中属于仓库 C 测试矩阵的一部分可使用仓库标准的 Bazel 测试命令构建运行依赖项包括自动生成的echo.grpc.pb.h/echo_mock.grpc.pb.h由 src/proto/grpc/testing/echo.proto 开启generate_mocks生成、grpcpp/test/mock_stream.h、gmock/gtest 与 abseil 日志库。在自己的项目里落地时只需把 3.1/3.2 中任一路径的 mock 生成打开将生成的*_mock.grpc.pb.h纳入测试 target并链接 gmock/gtest 即可复刻上述全部模式。六、要点速记Mock 对象是StubInterface的子类Unary 方法直接 mock流式方法 mock 其私有纯虚*Raw工厂流对象本身也要 mock直接复用 include/grpcpp/test/mock_stream.h 的MockClientReaderResponse、MockClientWriterRequest、MockClientReaderWriterRequest, Response生成方式二选一protoc 插件 flaggenerate_mock_codetrue或 Bazel 规则属性generate_mocks True产物均为服务_mock.grpc.pb.h典型断言手法Times控次数、WillOnce/WillRepeatedly编排行为、DoAll(SetArgPointeeN(...), Return(...))填出参、SaveArg截获入参、WithArg配合自定义ACTION_P回显消息、最后一级Read返回false模拟流结束最佳实践被测客户端面向StubInterface注入先跑真实 RPC 证明逻辑、再切 Mock 验证期望脚本参见 test/cpp/end2end/mock_test.cc。这套方法论让 gRPC C 客户端逻辑的单元测试摆脱了 server 依赖与网络不确定性测试速度快、确定性高、失败定位精确是 doc/unit_testing.md 给出的官方推荐做法并已在仓库的MockTest系列用例中成为可复制的标准范式。【免费下载链接】grpcC based gRPC (C, Python, Ruby, Objective-C, PHP, C#)项目地址: https://gitcode.com/GitHub_Trending/gr/grpc创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考