Before you start
Make sure that you can compile and run core tests and read a basic example Writing your first HTTP server.
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
We generate and link to a CMake library from our .proto
schema:
include(GrpcTargets)
userver_add_grpc_library(${PROJECT_NAME}-proto PROTOS samples/greeter.proto)
target_link_libraries(${PROJECT_NAME} ${PROJECT_NAME}-proto)
Register the necessary ugrpc
components:
.Append<ugrpc::client::ClientFactoryComponent>()
The client side
Wrap the generated api::GreeterServiceClient
in a component that exposes a simplified interface:
public:
static constexpr std::string_view kName = "greeter-client";
: LoggableComponentBase(config, context),
client_factory_(
context.FindComponent<
ugrpc::client::ClientFactoryComponent>()
.GetFactory()),
client_(client_factory_.MakeClient<api::GreeterServiceClient>(
"greeter", config[
"endpoint"].As<
std::string>())) {}
std::string SayHello(std::string name);
private:
api::GreeterServiceClient client_;
};
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) {
api::GreetingRequest request;
request.set_name(std::move(name));
auto context = std::make_unique<grpc::ClientContext>();
context->set_deadline(
auto stream = 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, a derived api::GreeterServiceBase::Component
class is provided for easy integration with the component system.
class GreeterServiceComponent final
: public api::GreeterServiceBase::Component {
public:
static constexpr std::string_view kName = "greeter-service";
: api::GreeterServiceBase::Component(config, context),
prefix_(config[
"greeting-prefix"].As<
std::string>()) {}
void SayHello(SayHelloCall& call, api::GreetingRequest&& request) override;
private:
const std::string prefix_;
};
A single request-response RPC handling is simple: fill in the response
and send it.
void GreeterServiceComponent::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<samples::http_cache::HttpCachedTranslations>()
.Append<samples::http_cache::GreetUser>()
.Append<server::handlers::TestsControl>()
.Append<components::HttpClient>();
}
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
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!'
Full sources
See the full example at: