userver: /data/code/service_template/third_party/userver/testsuite/pytest_plugins/pytest_userver/plugins/grpc/mockserver.py Source File
⚠️ This is the documentation for an old userver version. Click here to switch to the latest version.
All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Friends Macros Modules Pages Concepts
mockserver.py
1"""
2Mocks for the gRPC servers.
3
4@sa @ref scripts/docs/en/userver/tutorial/grpc_service.md
5"""
6
7# pylint: disable=no-member
8import asyncio
9import contextlib
10import functools
11import socket
12
13import grpc
14import pytest
15
16from testsuite.utils import callinfo
17
18DEFAULT_PORT = 8091
19
20
22 def __init__(self, servicer, methods):
23 self.servicer = servicer
24 self._known_methods = methods
25 self._methods = {}
26
27 def get(self, method, default):
28 return self._methods.get(method, default)
29
30 @contextlib.contextmanager
31 def mock(self):
32 try:
33 yield self.install_handler
34 finally:
35 self._methods = {}
36
37 def install_handler(self, method: str):
38 def decorator(func):
39 if method not in self._known_methods:
40 raise RuntimeError(
41 f'Trying to mock unknown grpc method {method}',
42 )
43
44 wrapped = callinfo.acallqueue(func)
45 self._methods[method] = wrapped
46 return wrapped
47
48 return decorator
49
50
51@pytest.fixture(scope='session')
52def _grpc_mockserver_endpoint(pytestconfig):
53 port = pytestconfig.option.grpc_mockserver_port
54 if pytestconfig.option.service_wait or pytestconfig.option.service_disable:
55 port = port or DEFAULT_PORT
56 if port == 0:
57 port = _find_free_port()
58 return f'{pytestconfig.option.grpc_mockserver_host}:{port}'
59
60
61@pytest.fixture(scope='session')
62def grpc_mockserver_endpoint(pytestconfig, _grpc_port) -> str:
63 """
64 Returns the gRPC endpoint to start the mocking server that is set by
65 command line `--grpc-mockserver-host` and `--grpc-mockserver-port` options.
66
67 Override this fixture to change the way the gRPC endpoint
68 is detected by the testsuite.
69
70 @snippet samples/grpc_service/tests/conftest.py Prepare configs
71 @ingroup userver_testsuite_fixtures
72 """
73 return f'{pytestconfig.option.grpc_mockserver_host}:{_grpc_port}'
74
75
76def _find_free_port() -> int:
77 with contextlib.closing(
78 socket.socket(socket.AF_INET6, socket.SOCK_STREAM),
79 ) as s:
80 s.bind(('', 0))
81 s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
82 return s.getsockname()[1]
83
84
85@pytest.fixture(scope='session')
86def _grpc_port(_grpc_mockserver_endpoint):
87 _, port = _grpc_mockserver_endpoint.rsplit(':', 1)
88 return int(port)
89
90
91@pytest.fixture(scope='session')
92async def grpc_mockserver(_grpc_mockserver_endpoint):
93 """
94 Returns the gRPC mocking server.
95
96 Override this fixture to change the way the gRPC
97 mocking server is started by the testsuite.
98
99 @snippet samples/grpc_service/tests/conftest.py Prepare server mock
100 @ingroup userver_testsuite_fixtures
101 """
102 server = grpc.aio.server()
103 server.add_insecure_port(_grpc_mockserver_endpoint)
104 server_task = asyncio.create_task(server.start())
105
106 try:
107 yield server
108 finally:
109 await server.stop(grace=None)
110 await server.wait_for_termination()
111 await server_task
112
113
114@pytest.fixture(scope='session')
116 """
117 Creates the gRPC mock server for the provided type.
118
119 @snippet samples/grpc_service/tests/conftest.py Prepare server mock
120 @ingroup userver_testsuite_fixtures
121 """
122 return _create_servicer_mock
123
124
125def pytest_addoption(parser):
126 group = parser.getgroup('grpc-mockserver')
127 group.addoption(
128 '--grpc-mockserver-host',
129 default='[::]',
130 help='gRPC mockserver hostname, default is [::]',
131 )
132 group.addoption(
133 '--grpc-mockserver-port',
134 type=int,
135 default=0,
136 help='gRPC mockserver port, by default random port is used',
137 )
138
139
140def _create_servicer_mock(servicer_class):
141 def wrap_grpc_method(name, default_method):
142 @functools.wraps(default_method)
143 async def run_method(self, *args, **kwargs):
144 method = mock.get(name, None)
145 if method is not None:
146 return await method(*args, **kwargs)
147 else:
148 return await default_method(self, *args, **kwargs)
149
150 return run_method
151
152 methods = {}
153 for attname, value in servicer_class.__dict__.items():
154 if callable(value):
155 methods[attname] = wrap_grpc_method(attname, value)
156
157 mocked_servicer_class = type(
158 f'Mock{servicer_class.__name__}', (servicer_class,), methods,
159 )
160 servicer = mocked_servicer_class()
161 mock = GrpcServiceMock(servicer, frozenset(methods))
162 return mock