Skip to content

Commit 4c4a05e

Browse files
authored
tests: restore 100% unit test coverage for 'google.cloud.bigtable.client' (#343)
Define gRPC channel options at module-scope, improving testability, discoverability Toward #335.
1 parent 47d03f7 commit 4c4a05e

File tree

2 files changed

+166
-27
lines changed

2 files changed

+166
-27
lines changed

packages/google-cloud-bigtable/google/cloud/bigtable/client.py

Lines changed: 28 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,13 @@
6767
READ_ONLY_SCOPE = "https://www.googleapis.com/auth/bigtable.data.readonly"
6868
"""Scope for reading table data."""
6969

70+
_GRPC_CHANNEL_OPTIONS = (
71+
("grpc.max_send_message_length", -1),
72+
("grpc.max_receive_message_length", -1),
73+
("grpc.keepalive_time_ms", 30000),
74+
("grpc.keepalive_timeout_ms", 10000),
75+
)
76+
7077

7178
def _create_gapic_client(client_class, client_options=None, transport=None):
7279
def inner(self):
@@ -195,11 +202,15 @@ def _get_scopes(self):
195202
return scopes
196203

197204
def _emulator_channel(self, transport, options):
198-
"""
199-
Creates a channel using self._credentials in a similar way to grpc.secure_channel but
200-
using grpc.local_channel_credentials() rather than grpc.ssh_channel_credentials()
201-
to allow easy connection to a local emulator.
202-
:return: grpc.Channel or grpc.aio.Channel
205+
"""Create a channel using self._credentials
206+
207+
Works in a similar way to ``grpc.secure_channel`` but using
208+
``grpc.local_channel_credentials`` rather than
209+
``grpc.ssh_channel_credentials`` to allow easy connection to a
210+
local emulator.
211+
212+
Returns:
213+
grpc.Channel or grpc.aio.Channel
203214
"""
204215
# TODO: Implement a special credentials type for emulator and use
205216
# "transport.create_channel" to create gRPC channels once google-auth
@@ -219,8 +230,8 @@ def _emulator_channel(self, transport, options):
219230
)
220231

221232
def _local_composite_credentials(self):
222-
"""
223-
Creates the credentials for the local emulator channel
233+
"""Create credentials for the local emulator channel.
234+
224235
:return: grpc.ChannelCredentials
225236
"""
226237
credentials = google.auth.credentials.with_scopes_if_required(
@@ -245,27 +256,24 @@ def _local_composite_credentials(self):
245256
)
246257

247258
def _create_gapic_client_channel(self, client_class, grpc_transport):
248-
options = {
249-
"grpc.max_send_message_length": -1,
250-
"grpc.max_receive_message_length": -1,
251-
"grpc.keepalive_time_ms": 30000,
252-
"grpc.keepalive_timeout_ms": 10000,
253-
}.items()
254-
if self._client_options and self._client_options.api_endpoint:
259+
if self._emulator_host is not None:
260+
api_endpoint = self._emulator_host
261+
elif self._client_options and self._client_options.api_endpoint:
255262
api_endpoint = self._client_options.api_endpoint
256263
else:
257264
api_endpoint = client_class.DEFAULT_ENDPOINT
258265

259-
channel = None
260266
if self._emulator_host is not None:
261-
api_endpoint = self._emulator_host
262-
channel = self._emulator_channel(grpc_transport, options)
267+
channel = self._emulator_channel(
268+
transport=grpc_transport, options=_GRPC_CHANNEL_OPTIONS,
269+
)
263270
else:
264271
channel = grpc_transport.create_channel(
265-
host=api_endpoint, credentials=self._credentials, options=options,
272+
host=api_endpoint,
273+
credentials=self._credentials,
274+
options=_GRPC_CHANNEL_OPTIONS,
266275
)
267-
transport = grpc_transport(channel=channel, host=api_endpoint)
268-
return transport
276+
return grpc_transport(channel=channel, host=api_endpoint)
269277

270278
@property
271279
def project_path(self):

packages/google-cloud-bigtable/tests/unit/test_client.py

Lines changed: 138 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,7 @@ def test_constructor_both_admin_and_read_only(self):
170170

171171
def test_constructor_with_emulator_host(self):
172172
from google.cloud.environment_vars import BIGTABLE_EMULATOR
173+
from google.cloud.bigtable.client import _GRPC_CHANNEL_OPTIONS
173174

174175
credentials = _make_credentials()
175176
emulator_host = "localhost:8081"
@@ -183,13 +184,9 @@ def test_constructor_with_emulator_host(self):
183184
client.table_data_client
184185

185186
self.assertEqual(client._emulator_host, emulator_host)
186-
options = {
187-
"grpc.max_send_message_length": -1,
188-
"grpc.max_receive_message_length": -1,
189-
"grpc.keepalive_time_ms": 30000,
190-
"grpc.keepalive_timeout_ms": 10000,
191-
}.items()
192-
factory.assert_called_once_with(emulator_host, credentials, options=options)
187+
factory.assert_called_once_with(
188+
emulator_host, credentials, options=_GRPC_CHANNEL_OPTIONS,
189+
)
193190

194191
def test__get_scopes_default(self):
195192
from google.cloud.bigtable.client import DATA_SCOPE
@@ -215,6 +212,140 @@ def test__get_scopes_read_only(self):
215212
)
216213
self.assertEqual(client._get_scopes(), (READ_ONLY_SCOPE,))
217214

215+
def test__emulator_channel_sync(self):
216+
emulator_host = "localhost:8081"
217+
transport_name = "GrpcTransportTesting"
218+
transport = mock.Mock(spec=["__name__"], __name__=transport_name)
219+
options = mock.Mock(spec=[])
220+
client = self._make_one(
221+
project=self.PROJECT, credentials=_make_credentials(), read_only=True
222+
)
223+
client._emulator_host = emulator_host
224+
lcc = client._local_composite_credentials = mock.Mock(spec=[])
225+
226+
with mock.patch("grpc.secure_channel") as patched:
227+
channel = client._emulator_channel(transport, options)
228+
229+
assert channel is patched.return_value
230+
patched.assert_called_once_with(
231+
emulator_host, lcc.return_value, options=options,
232+
)
233+
234+
def test__emulator_channel_async(self):
235+
emulator_host = "localhost:8081"
236+
transport_name = "GrpcAsyncIOTransportTesting"
237+
transport = mock.Mock(spec=["__name__"], __name__=transport_name)
238+
options = mock.Mock(spec=[])
239+
client = self._make_one(
240+
project=self.PROJECT, credentials=_make_credentials(), read_only=True
241+
)
242+
client._emulator_host = emulator_host
243+
lcc = client._local_composite_credentials = mock.Mock(spec=[])
244+
245+
with mock.patch("grpc.aio.secure_channel") as patched:
246+
channel = client._emulator_channel(transport, options)
247+
248+
assert channel is patched.return_value
249+
patched.assert_called_once_with(
250+
emulator_host, lcc.return_value, options=options,
251+
)
252+
253+
def test__local_composite_credentials(self):
254+
client = self._make_one(
255+
project=self.PROJECT, credentials=_make_credentials(), read_only=True
256+
)
257+
258+
wsir_patch = mock.patch("google.auth.credentials.with_scopes_if_required")
259+
request_patch = mock.patch("google.auth.transport.requests.Request")
260+
amp_patch = mock.patch("google.auth.transport.grpc.AuthMetadataPlugin")
261+
grpc_patches = mock.patch.multiple(
262+
"grpc",
263+
metadata_call_credentials=mock.DEFAULT,
264+
local_channel_credentials=mock.DEFAULT,
265+
composite_channel_credentials=mock.DEFAULT,
266+
)
267+
with wsir_patch as wsir_patched:
268+
with request_patch as request_patched:
269+
with amp_patch as amp_patched:
270+
with grpc_patches as grpc_patched:
271+
credentials = client._local_composite_credentials()
272+
273+
grpc_mcc = grpc_patched["metadata_call_credentials"]
274+
grpc_lcc = grpc_patched["local_channel_credentials"]
275+
grpc_ccc = grpc_patched["composite_channel_credentials"]
276+
277+
self.assertIs(credentials, grpc_ccc.return_value)
278+
279+
wsir_patched.assert_called_once_with(client._credentials, None)
280+
request_patched.assert_called_once_with()
281+
amp_patched.assert_called_once_with(
282+
wsir_patched.return_value, request_patched.return_value,
283+
)
284+
grpc_mcc.assert_called_once_with(amp_patched.return_value)
285+
grpc_lcc.assert_called_once_with()
286+
grpc_ccc.assert_called_once_with(grpc_lcc.return_value, grpc_mcc.return_value)
287+
288+
def _create_gapic_client_channel_helper(
289+
self, endpoint=None, emulator_host=None,
290+
):
291+
from google.cloud.bigtable.client import _GRPC_CHANNEL_OPTIONS
292+
293+
client_class = mock.Mock(spec=["DEFAULT_ENDPOINT"])
294+
credentials = _make_credentials()
295+
client = self._make_one(project=self.PROJECT, credentials=credentials)
296+
297+
if endpoint is not None:
298+
client._client_options = mock.Mock(
299+
spec=["api_endpoint"], api_endpoint=endpoint,
300+
)
301+
expected_host = endpoint
302+
else:
303+
expected_host = client_class.DEFAULT_ENDPOINT
304+
305+
if emulator_host is not None:
306+
client._emulator_host = emulator_host
307+
client._emulator_channel = mock.Mock(spec=[])
308+
expected_host = emulator_host
309+
310+
grpc_transport = mock.Mock(spec=["create_channel"])
311+
312+
transport = client._create_gapic_client_channel(client_class, grpc_transport)
313+
314+
self.assertIs(transport, grpc_transport.return_value)
315+
316+
if emulator_host is not None:
317+
client._emulator_channel.assert_called_once_with(
318+
transport=grpc_transport, options=_GRPC_CHANNEL_OPTIONS,
319+
)
320+
grpc_transport.assert_called_once_with(
321+
channel=client._emulator_channel.return_value, host=expected_host,
322+
)
323+
else:
324+
grpc_transport.create_channel.assert_called_once_with(
325+
host=expected_host,
326+
credentials=client._credentials,
327+
options=_GRPC_CHANNEL_OPTIONS,
328+
)
329+
grpc_transport.assert_called_once_with(
330+
channel=grpc_transport.create_channel.return_value, host=expected_host,
331+
)
332+
333+
def test__create_gapic_client_channel_w_defaults(self):
334+
self._create_gapic_client_channel_helper()
335+
336+
def test__create_gapic_client_channel_w_endpoint(self):
337+
endpoint = "api.example.com"
338+
self._create_gapic_client_channel_helper(endpoint=endpoint)
339+
340+
def test__create_gapic_client_channel_w_emulator_host(self):
341+
host = "api.example.com:1234"
342+
self._create_gapic_client_channel_helper(emulator_host=host)
343+
344+
def test__create_gapic_client_channel_w_endpoint_w_emulator_host(self):
345+
endpoint = "api.example.com"
346+
host = "other.example.com:1234"
347+
self._create_gapic_client_channel_helper(endpoint=endpoint, emulator_host=host)
348+
218349
def test_project_path_property(self):
219350
credentials = _make_credentials()
220351
project = "PROJECT"

0 commit comments

Comments
 (0)