Before you start
Make sure that you can compile and run core tests and read a basic example Writing your first HTTP server.
Make sure that you understand the basic concepts of userver grpc driver.
Step by step guide
In this example, we will write a client side and a server side for a simple GreeterService
from greeter.proto
(see the schema below). The service has 4 methods. Its methods accept a name
and reply with a greeting
: samples/grpc_service/proto/samples/greeter.proto
Installation
Find and link to userver gRPC:
find_package(userver COMPONENTS grpc REQUIRED)
add_library(${PROJECT_NAME}_objs OBJECT
# Note: it's nonsense to have the same client and service in the same executable.
# For test and demonstration purposes only.
src/greeter_client.cpp
src/greeter_service.cpp
)
target_link_libraries(${PROJECT_NAME}_objs PUBLIC userver::grpc)
target_include_directories(${PROJECT_NAME}_objs PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/src)
Generate and link to a CMake library from our .proto
schema:
userver_add_grpc_library(${PROJECT_NAME}-proto PROTOS samples/greeter.proto)
target_link_libraries(${PROJECT_NAME}_objs PUBLIC ${PROJECT_NAME}-proto)
By default, userver_add_grpc_library
looks in ${CMAKE_CURRENT_SOURCE_DIR}/proto
, you can override this using the SOURCE_PATH
option.
Proto includes can be specified in INCLUDE_DIRECTORIES
option (multiple directories can be specified).
The client side
Wrap the generated api::GreeterServiceClient
in a component that exposes a simplified interface:
#include <samples/greeter_client.usrv.pb.hpp>
class GreeterClient final {
public:
explicit GreeterClient(api::GreeterServiceClient&& raw_client);
std::string SayHello(std::string name) const;
std::vector<std::string> SayHelloResponseStream(std::string name) const;
std::string SayHelloRequestStream(const std::vector<std::string_view>& names) const;
std::vector<std::string> SayHelloStreams(const std::vector<std::string_view>& names) const;
private:
static std::unique_ptr<grpc::ClientContext> MakeClientContext();
api::GreeterServiceClient raw_client_;
};
public:
static constexpr std::string_view kName = "greeter-client";
GreeterClientComponent(const ::components::ComponentConfig& config, const ::components::ComponentContext& context)
: Base(config, context) {}
using Base::GetClient;
};
We intentionally split GreeterClient
from GreeterClientComponent
to make the logic unit-testable. If you don't need gtest tests, you can put the logic into the component directly.
Single request - single response RPC handling is simple: fill in request
and context
, initiate the RPC, receive the response
.
std::string GreeterClient::SayHello(std::string name) const {
api::GreetingRequest request;
request.set_name(std::move(name));
auto stream = raw_client_.SayHello(request, MakeClientContext());
api::GreetingResponse response = stream.Finish();
return std::move(*response.mutable_greeting());
}
std::unique_ptr<grpc::ClientContext> GreeterClient::MakeClientContext() {
auto context = std::make_unique<grpc::ClientContext>();
return context;
}
Single request - stream response RPC handling:
std::vector<std::string> GreeterClient::SayHelloResponseStream(std::string name) const {
api::GreetingRequest request;
request.set_name(std::move(name));
auto stream = raw_client_.SayHelloResponseStream(request, MakeClientContext());
api::GreetingResponse response;
std::vector<std::string> result;
constexpr auto kCountSend = 5;
for (int i = 0; i < kCountSend; i++) {
if (!stream.Read(response)) {
}
result.push_back(std::move(*response.mutable_greeting()));
}
if (stream.Read(response)) {
}
return result;
}
Stream request - single response RPC handling:
std::string GreeterClient::SayHelloRequestStream(const std::vector<std::string_view>& names) const {
auto stream = raw_client_.SayHelloRequestStream(MakeClientContext());
for (const auto& name : names) {
api::GreetingRequest request;
request.set_name(grpc::string(name));
stream.WriteAndCheck(request);
}
auto response = stream.Finish();
return std::move(*response.mutable_greeting());
}
Stream request - stream response RPC handling:
std::vector<std::string> GreeterClient::SayHelloStreams(const std::vector<std::string_view>& names) const {
auto stream = raw_client_.SayHelloStreams(MakeClientContext());
std::vector<std::string> result;
api::GreetingResponse response;
for (const auto& name : names) {
api::GreetingRequest request;
request.set_name(grpc::string(name));
stream.WriteAndCheck(request);
if (!stream.Read(response)) {
}
result.push_back(std::move(*response.mutable_greeting()));
}
const bool is_success = stream.WritesDone();
LOG_DEBUG() <<
"Write task finish: " << is_success;
if (stream.Read(response)) {
}
return result;
}
Fill in the static config entries for the client side:
# yaml
# Contains machinery common to all gRPC clients
grpc-client-common:
# The TaskProcessor for blocking connection initiation
blocking-task-processor: grpc-blocking-task-processor
# Creates gRPC clients
grpc-client-factory:
# The list of gRPC client middleware components to use
middlewares: []
# Optional channel parameters for gRPC Core
# https://grpc.github.io/grpc/core/group__grpc__arg__keys.html
channel-args: {}
# Our wrapper around the generated client for GreeterService
greeter-client:
# The service endpoint (URI). We talk to our own service,
# which is kind of pointless, but works for an example
endpoint: '[::1]:8091'
client-name: greeter
dedicated-channel-counts:
SayHello: 3
SayHelloRequestStream: 2
factory-component: grpc-client-factory
# yaml
task_processors:
grpc-blocking-task-processor: # For blocking gRPC channel creation
worker_threads: 2
thread_name: grpc-worker
The server side
Implement the generated api::GreeterServiceBase
. As a convenience, api::GreeterServiceBase::Component
base class is provided that inherits from both api::GreeterServiceBase
and ugrpc::server::ServiceComponentBase
. However, for in this example we will also test our service using gtest, so we need to split the logic from the component.
#include <samples/greeter_service.usrv.pb.hpp>
class GreeterService final : public api::GreeterServiceBase {
public:
explicit GreeterService(std::string prefix);
SayHelloResult SayHello(CallContext& context, api::GreetingRequest&& request) override;
SayHelloResponseStreamResult SayHelloResponseStream(
CallContext& context,
api::GreetingRequest&& request,
SayHelloResponseStreamWriter& writer
) override;
SayHelloRequestStreamResult SayHelloRequestStream(CallContext& context, SayHelloRequestStreamReader& reader)
override;
SayHelloStreamsResult SayHelloStreams(CallContext& context, SayHelloStreamsReaderWriter& stream) override;
private:
const std::string prefix_;
};
public:
static constexpr std::string_view kName = "greeter-service";
private:
GreeterService service_;
};
GreeterServiceComponent::GreeterServiceComponent(
)
:
ugrpc::server::ServiceComponentBase(config, context), service_(config[
"greeting-prefix"].As<
std::string>()) {
RegisterService(service_);
}
Single request - single response RPC handling is simple: fill in the response
and send it.
GreeterService::SayHelloResult GreeterService::SayHello(CallContext& , api::GreetingRequest&& request) {
api::GreetingResponse response;
response.set_greeting(fmt::format("{}, {}!", prefix_, request.name()));
return response;
}
Single request - stream response RPC handling:
GreeterService::SayHelloResponseStreamResult GreeterService::SayHelloResponseStream(
CallContext& ,
api::GreetingRequest&& request,
SayHelloResponseStreamWriter& writer
) {
std::string message = fmt::format("{}, {}", prefix_, request.name());
api::GreetingResponse response;
constexpr auto kCountSend = 5;
for (auto i = 0; i < kCountSend; ++i) {
message.push_back('!');
response.set_greeting(grpc::string(message));
writer.Write(response);
}
return grpc::Status::OK;
}
Stream request - single response RPC handling:
GreeterService::SayHelloRequestStreamResult
GreeterService::SayHelloRequestStream(CallContext& , SayHelloRequestStreamReader& reader) {
std::string message{};
api::GreetingRequest request;
while (reader.Read(request)) {
message.append(request.name());
}
api::GreetingResponse response;
response.set_greeting(fmt::format("{}, {}", prefix_, message));
return response;
}
Stream request - stream response RPC handling:
GreeterService::SayHelloStreamsResult
GreeterService::SayHelloStreams(CallContext& , SayHelloStreamsReaderWriter& stream) {
std::string message;
api::GreetingRequest request;
api::GreetingResponse response;
while (stream.Read(request)) {
message.append(request.name());
response.set_greeting(fmt::format("{}, {}", prefix_, message));
stream.Write(response);
}
return grpc::Status::OK;
}
Fill in the static config entries for the server side:
# yaml
# Common configuration for gRPC server
grpc-server:
# The single listening port for incoming RPCs
port: $grpc_server_port
port#fallback: 8091
completion-queue-count: 3
# Our GreeterService implementation
greeter-service:
task-processor: main-task-processor
greeting-prefix: Hello
middlewares: []
int main()
Finally, we register our components and start the server.
int main(int argc, char* argv[]) {
.Append<ugrpc::client::CommonComponent>()
.Append<ugrpc::server::ServerComponent>()
.Append<samples::GreeterClientComponent>()
.Append<samples::GreeterServiceComponent>()
.Append<samples::CallGreeterClientTestHandler>();
}
Build and Run
To build the sample, execute the following build steps at the userver root directory:
mkdir build_release
cd build_release
cmake -DCMAKE_BUILD_TYPE=Release ..
make userver-samples-grpc_service
The sample could be started by running make start-userver-samples-grpc_service
. The command would invoke testsuite start target that sets proper paths in the configuration files and starts the service.
To start the service manually run ./samples/grpc_service/userver-samples-grpc_service -c </path/to/static_config.yaml>
.
The service is available locally at port 8091 (as per our static_config.yaml
).
Functional testing for the sample gRPC service and client
To implement Functional tests for the service some preparational steps should be done.
Preparations
First of all, import the required modules and add the required pytest_userver.plugins.grpc pytest plugin:
import pytest
import samples.greeter_pb2_grpc as greeter_services
pytest_plugins = ['pytest_userver.plugins.grpc']
gRPC server mock
To mock the gRPC server provide a hook for the static config to change the endpoint:
USERVER_CONFIG_HOOKS = ['prepare_service_config']
@pytest.fixture(scope='session')
def prepare_service_config(grpc_mockserver_endpoint):
def patch_config(config, config_vars):
components = config['components_manager']['components']
components['greeter-client']['endpoint'] = grpc_mockserver_endpoint
return patch_config
Write the mocking fixtures using grpc_mockserver:
@pytest.fixture(name='mock_grpc_greeter_session', scope='session')
def _mock_grpc_greeter_session(grpc_mockserver, create_grpc_mock):
mock = create_grpc_mock(
greeter_services.GreeterServiceServicer,
stream_method_names=['SayHelloResponseStream', 'SayHelloStreams'],
)
greeter_services.add_GreeterServiceServicer_to_server(
mock.servicer, grpc_mockserver,
)
return mock
@pytest.fixture
def mock_grpc_greeter(mock_grpc_greeter_session):
with mock_grpc_greeter_session.mock() as mock:
yield mock
After that everything is ready to check single request - single response service client requests:
async def test_grpc_client_mock_say_hello(service_client, mock_grpc_greeter):
@mock_grpc_greeter('SayHello')
async def _mock_say_hello(request, context):
return greeter_protos.GreetingResponse(
greeting=f'Hello, {request.name} from mockserver!',
)
response = await service_client.post('/hello?case=say_hello', data='tests')
assert response.status == 200
assert 'text/plain' in response.headers['Content-Type']
assert response.text == 'Hello, tests from mockserver!'
assert _mock_say_hello.times_called == 1
To check single request - stream response service client requests:
async def test_grpc_client_mock_say_hello_response_stream(
service_client, mock_grpc_greeter,
):
@mock_grpc_greeter('SayHelloResponseStream')
async def _mock_say_hello_response_stream(request, context):
message = f'Hello, {request.name}'
for i in range(5):
message += '!'
yield greeter_protos.GreetingResponse(greeting=message)
response = await service_client.post(
'/hello?case=say_hello_response_stream', data='Python',
)
assert response.status == 200
assert 'text/plain' in response.headers['Content-Type']
assert (
response.text
== """Hello, Python!
Hello, Python!!
Hello, Python!!!
Hello, Python!!!!
Hello, Python!!!!!
"""
)
assert _mock_say_hello_response_stream.times_called == 1
To check stream request - single response service client requests:
async def test_grpc_client_mock_say_hello_request_stream(
service_client, mock_grpc_greeter,
):
@mock_grpc_greeter('SayHelloRequestStream')
async def _mock_say_hello_request_stream(request_iterator, context):
message = 'Hello, '
async for request in request_iterator:
message += f'{request.name}'
return greeter_protos.GreetingResponse(greeting=message)
response = await service_client.post(
'/hello?case=say_hello_request_stream', data='Python\n!\n!\n!',
)
assert response.status == 200
assert 'text/plain' in response.headers['Content-Type']
assert response.text == 'Hello, Python!!!'
assert _mock_say_hello_request_stream.times_called == 1
To check stream request - stream response service client requests:
async def test_grpc_client_mock_say_hello_streams(
service_client, mock_grpc_greeter,
):
@mock_grpc_greeter('SayHelloStreams')
async def _mock_say_hello_streams(request_iterator, context):
message = 'Hello, '
async for request in request_iterator:
message += f'{request.name}'
yield greeter_protos.GreetingResponse(greeting=message)
response = await service_client.post(
'/hello?case=say_hello_streams', data='Python\n!\n!\n!',
)
assert response.status == 200
assert 'text/plain' in response.headers['Content-Type']
assert (
response.text
== """Hello, Python
Hello, Python!
Hello, Python!!
Hello, Python!!!
"""
)
assert _mock_say_hello_streams.times_called == 1
gRPC client
To do the gRPC requests write a client fixture using grpc_channel:
@pytest.fixture
def grpc_client(grpc_channel, service_client):
return greeter_services.GreeterServiceStub(grpc_channel)
Use it to do single request - single response gRPC requests to the service:
async def test_say_hello(grpc_client):
request = greeter_protos.GreetingRequest(name='Python')
response = await grpc_client.SayHello(request)
assert response.greeting == 'Hello, Python!'
To do single request - stream response gRPC requests to the service:
async def test_say_hello_response_stream(grpc_client):
request = greeter_protos.GreetingRequest(name='Python')
message = 'Hello, Python'
async for response in grpc_client.SayHelloResponseStream(request):
message += '!'
assert response.greeting == message
To do stream request - single response gRPC requests to the service:
async def test_say_hello_request_stream(grpc_client):
request = (
greeter_protos.GreetingRequest(name=name)
for name in ['Python', '!', '!', '!']
)
response = await grpc_client.SayHelloRequestStream(request)
assert response.greeting == 'Hello, Python!!!'
To do stream request - stream response gRPC requests to the service:
async def test_say_hello_streams(grpc_client):
requests = (
greeter_protos.GreetingRequest(name=name)
for name in ['Python', '!', '!', '!']
)
message = 'Hello, Python'
async for response in grpc_client.SayHelloStreams(requests):
assert response.greeting == message
message += '!'
Unit testing for the sample gRPC service and client (gtest)
To implement unit testing for the sample gRPC service and client some preparational steps should be done.
Preparations
First, link the unit tests to userver::grpc-utest
:
add_executable(${PROJECT_NAME}-unittest unittests/greeter_service_test.cpp)
target_link_libraries(${PROJECT_NAME}-unittest
${PROJECT_NAME}_objs
userver::utest
)
add_google_tests(${PROJECT_NAME}-unittest)
Create a fixture that sets up the gRPC service in unit tests:
protected:
GreeterServiceTest() : service_(prefix_) {
}
private:
const std::string prefix_{"Hello"};
samples::GreeterService service_;
};
Unit testing for the gRPC service
Finally, we can create gRPC service and client in unit tests. To do single request - single response requests:
UTEST_F(GreeterServiceTest, SayHelloDirectCall) {
const auto client = MakeClient<samples::api::GreeterServiceClient>();
samples::api::GreetingRequest request;
request.set_name("gtest");
const auto response = client.SayHello(request).Finish();
EXPECT_EQ(response.greeting(), "Hello, gtest!");
}
UTEST_F(GreeterServiceTest, SayHelloCustomClient) {
const samples::GreeterClient client{MakeClient<samples::api::GreeterServiceClient>()};
const auto response = client.SayHello("gtest");
EXPECT_EQ(response, "Hello, gtest!");
}
To do single request - stream response service client requests:
UTEST_F(GreeterServiceTest, SayHelloResponseStreamCustomClient) {
const samples::GreeterClient client{MakeClient<samples::api::GreeterServiceClient>()};
const auto responses = client.SayHelloResponseStream("gtest");
EXPECT_THAT(
responses,
testing::ElementsAre(
"Hello, gtest!", "Hello, gtest!!", "Hello, gtest!!!", "Hello, gtest!!!!", "Hello, gtest!!!!!"
)
);
}
To do stream request - single response service client requests:
UTEST_F(GreeterServiceTest, SayHelloRequestStreamCustomClient) {
const samples::GreeterClient client{MakeClient<samples::api::GreeterServiceClient>()};
const std::vector<std::string_view> names = {"gtest", "!", "!", "!"};
const auto response = client.SayHelloRequestStream(names);
EXPECT_EQ(response, "Hello, gtest!!!");
}
To do stream request - stream response service client requests:
UTEST_F(GreeterServiceTest, SayHelloStreamsCustomClient) {
const samples::GreeterClient client{MakeClient<samples::api::GreeterServiceClient>()};
const std::vector<std::string_view> names = {"gtest", "!", "!", "!"};
const auto responses = client.SayHelloStreams(names);
EXPECT_THAT(responses, testing::ElementsAre("Hello, gtest", "Hello, gtest!", "Hello, gtest!!", "Hello, gtest!!!"));
}
Unit testing for the gRPC client
We can use toy test-only services to test the service client as a unit.
Testing single request - single response client RPC-handling:
namespace {
class GreeterMock final : public samples::api::GreeterServiceBase {
public:
SayHelloResult SayHello(CallContext& , ::samples::api::GreetingRequest&& ) override {
samples::api::GreetingResponse response;
response.set_greeting("Mocked response");
return response;
}
SayHelloResponseStreamResult SayHelloResponseStream(
CallContext& context,
::samples::api::GreetingRequest&& request,
SayHelloResponseStreamWriter& writer
) override;
SayHelloRequestStreamResult SayHelloRequestStream(CallContext& context, SayHelloRequestStreamReader& reader)
override;
SayHelloStreamsResult SayHelloStreams(CallContext& context, SayHelloStreamsReaderWriter& stream) override;
};
}
UTEST_F(GreeterClientTest, SayHelloMockedServiceCustomClient) {
const samples::GreeterClient client{MakeClient<samples::api::GreeterServiceClient>()};
const auto response = client.SayHello("gtest");
EXPECT_EQ(response, "Mocked response");
}
Testing single request - stream response client RPC-handling:
GreeterMock::SayHelloResponseStreamResult GreeterMock::SayHelloResponseStream(
CallContext& ,
::samples::api::GreetingRequest&& ,
SayHelloResponseStreamWriter& writer
) {
samples::api::GreetingResponse response;
std::string message = "Mocked response";
int kCountSend = 5;
for (auto i = 0; i < kCountSend; ++i) {
message.push_back('!');
response.set_greeting(grpc::string(message));
writer.Write(response);
}
return grpc::Status::OK;
}
UTEST_F(GreeterClientTest, SayHelloResponseStreamMockedServiceCustomClient) {
const samples::GreeterClient client{MakeClient<samples::api::GreeterServiceClient>()};
const auto responses = client.SayHelloResponseStream("gtest");
EXPECT_THAT(
responses,
testing::ElementsAre(
"Mocked response!", "Mocked response!!", "Mocked response!!!", "Mocked response!!!!", "Mocked response!!!!!"
)
);
}
Testing stream request - single response client RPC-handling:
GreeterMock::SayHelloRequestStreamResult
GreeterMock::SayHelloRequestStream(CallContext& , SayHelloRequestStreamReader& reader) {
samples::api::GreetingResponse response;
samples::api::GreetingRequest request;
while (reader.Read(request)) {
}
response.set_greeting("Mocked response!!!");
return response;
}
UTEST_F(GreeterClientTest, SayHelloRequestStreamMockedServiceCustomClient) {
const samples::GreeterClient client{MakeClient<samples::api::GreeterServiceClient>()};
const std::vector<std::string_view> names = {"gtest", "!", "!", "!"};
auto response = client.SayHelloRequestStream(names);
EXPECT_EQ(response, "Mocked response!!!");
}
Testing stream request - stream response client RPC-handling:
GreeterMock::SayHelloStreamsResult
GreeterMock::SayHelloStreams(CallContext& , SayHelloStreamsReaderWriter& stream) {
samples::api::GreetingResponse response;
std::string message = "Mocked response";
samples::api::GreetingRequest request;
while (stream.Read(request)) {
response.set_greeting(grpc::string(message));
stream.Write(response);
message.push_back('!');
}
return grpc::Status::OK;
}
UTEST_F(GreeterClientTest, SayHelloStreamsMockedServiceCustomClient) {
const samples::GreeterClient client{MakeClient<samples::api::GreeterServiceClient>()};
const std::vector<std::string_view> names = {"gtest", "!", "!", "!"};
const auto responses = client.SayHelloStreams(names);
EXPECT_THAT(
responses,
testing::ElementsAre("Mocked response", "Mocked response!", "Mocked response!!", "Mocked response!!!")
);
}
Full sources
See the full example at: