From 7431a78628ffcd86fd7e7512610ad118c16eb6f1 Mon Sep 17 00:00:00 2001 From: MakMukhi Date: Wed, 3 May 2017 17:50:31 -0700 Subject: How to write unit tests for client sync APIs. --- doc/unit_testing.md | 174 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 174 insertions(+) (limited to 'doc/unit_testing.md') diff --git a/doc/unit_testing.md b/doc/unit_testing.md index 8b13789179..3307d11587 100644 --- a/doc/unit_testing.md +++ b/doc/unit_testing.md @@ -1 +1,175 @@ +# How to write unit tests for gRPC C++ client. + +tl;dr: [Example code](https://github.com/grpc/grpc/blob/master/test/cpp/end2end/mock_test.cc). + +To unit-test client-side logic via the synchronous API, gRPC provides a mocked Stub that can be programmed upon and easily incorporated in the test code. + +For instance, consider an EchoService like this: + + +```proto +service EchoTestService { +        rpc Echo(EchoRequest) returns (EchoResponse); +        rpc BidiStream(stream EchoRequest) returns (stream EchoResponse); +} +``` + +The code generate would look something like this: + +```c++ +class EchoTestService final { + public: + class StubInterface { + virtual ::grpc::Status Echo(::grpc::ClientContext* context, const ::grpc::testing::EchoRequest& request, ::grpc::testing::EchoResponse* response) = 0; + … + 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: + virtual ::grpc::ClientReaderWriterInterface< ::grpc::testing::EchoRequest, ::grpc::testing::EchoResponse>* BidiStreamRaw(::grpc::ClientContext* context) = 0; + … +  } // End StubInterface +… +} // End EchoTestService +``` + + +If we mock the StubInterface and set expectations on the pure-virtual methods we can test client-side logic without having to make any rpcs. + +A mock for this StubInterface will look like this: + + +```c++ +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)); +}; +``` + + +**Generating mock code:** + +Such a mock can be auto-generated by: + + + +1. Setting flag(generate_mock_code=true) on grpc plugin for protoc, or +1. Setting an attribute(generate_mock) in your bazel rule. + +Protoc plugin flag: + +```sh +protoc -I . --grpc_out=generate_mock_code=true:. --plugin=protoc-gen-grpc=`which grpc_cpp_plugin` echo.proto +``` + +Bazel rule: + +```py +grpc_proto_library( + name = "echo_proto", + srcs = ["echo.proto"], + generate_mock = True, +) +``` + + +By adding such a flag now a header file `echo_mock.h` containing the mocked stub will also be generated. + +This header file can then be included in test files along with a gmock dependency. + +**Writing tests with mocked Stub.** + +Consider the following client a user might have: + +```c++ +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; + grpc::string msg("hello"); + + std::unique_ptr> + 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_; +}; +``` + +A test could initialize this FakeClient with a mocked stub having set expectations on it: + +Unary RPC: + +```c++ +MockEchoTestServiceStub stub; +EchoResponse resp; +resp.set_message("hello world"); +Expect_CALL(stub, Echo(_,_,_)).Times(Atleast(1)).WillOnce(DoAll(SetArgPointee<2>(resp), Return(Status::OK))); +FakeClient client(stub); +client.DoEcho(); +``` + +Streaming RPC: + +```c++ +ACTION_P(copy, msg) { + arg0->set_message(msg->message()); +} + + +auto rw = new MockClientReaderWriter(); +EchoRequest msg; +EXPECT_CALL(*rw, Write(_, _)).Times(3).WillRepeatedly(DoAll(SaveArg<0>(&msg), Return(true))); +EXPECT_CALL(*rw, Read(_)). + WillOnce(DoAll(WithArg<0>(copy(&msg)), Return(true))). + WillOnce(DoAll(WithArg<0>(copy(&msg)), Return(true))). + WillOnce(DoAll(WithArg<0>(copy(&msg)), Return(true))). + WillOnce(Return(false)); + +MockEchoTestServiceStub stub; +EXPECT_CALL(stub, BidiStreamRaw(_)).Times(AtLeast(1)).WillOnce(Return(rw)); + +FakeClient client(stub); +client.DoBidiStream(); +``` -- cgit v1.2.3