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). Its single SayHello
method accepts a name
string and replies with a corresponding greeting
string.
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;
private:
api::GreeterServiceClient raw_client_;
};
public:
static constexpr std::string_view kName = "greeter-client";
const GreeterClient& GetClient() const;
private:
GreeterClient client_;
};
GreeterClientComponent::GreeterClientComponent(
: ComponentBase(config, context),
client_factory_(
context.FindComponent<
ugrpc::client::ClientFactoryComponent>()
.GetFactory()),
client_(client_factory_.MakeClient<api::GreeterServiceClient>(
"greeter", config[
"endpoint"].As<
std::string>())) {}
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.
A single request-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 context = std::make_unique<grpc::ClientContext>();
context->set_deadline(
auto stream = raw_client_.SayHello(request, std::move(context));
api::GreetingResponse response = stream.Finish();
return std::move(*response.mutable_greeting());
}
Fill in the static config entries for the client side:
# yaml
# Creates gRPC clients
grpc-client-factory:
# The TaskProcessor for blocking connection initiation
task-processor: grpc-blocking-task-processor
# 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'
# 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);
void SayHello(SayHelloCall& call, api::GreetingRequest&& request) override;
private:
const std::string prefix_;
};
class GreeterServiceComponent final
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_);
}
A single request-response RPC handling is simple: fill in the response
and send it.
void GreeterService::SayHello(api::GreeterServiceBase::SayHelloCall& call,
api::GreetingRequest&& request) {
api::GreetingResponse response;
response.set_greeting(fmt::format("{}, {}!", prefix_, request.name()));
call.Finish(response);
}
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[]) {
const auto component_list =
.Append<ugrpc::client::ClientFactoryComponent>()
.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)
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 the service client requests:
async def test_grpc_client(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', data='tests', headers={'Content-type': 'text/plain'},
)
assert response.status == 200
assert response.content == b'Hello, tests from mockserver!'
assert _mock_say_hello.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 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!'
Unit testing for the sample gRPC service and client (gtest)
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_;
};
Finally, we can create gRPC service and client in unit tests:
UTEST_F(GreeterServiceTest, DirectCall) {
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, CustomClient) {
const samples::GreeterClient client{
MakeClient<samples::api::GreeterServiceClient>()};
const auto response = client.SayHello("gtest");
EXPECT_EQ(response, "Hello, gtest!");
}
We can also use toy test-only gRPC services for unit tests:
namespace {
class GreeterMock final : public samples::api::GreeterServiceBase {
public:
void SayHello(SayHelloCall& call,
samples::api::GreetingRequest&& ) override {
samples::api::GreetingResponse response;
response.set_greeting("Mocked response");
call.Finish(response);
}
};
}
UTEST_F(GreeterClientTest, MockedServiceCustomClient) {
const samples::GreeterClient client{
MakeClient<samples::api::GreeterServiceClient>()};
const auto response = client.SayHello("gtest");
EXPECT_EQ(response, "Mocked response");
}
Full sources
See the full example at: