Skip to content

Conversation

@theacodes
Copy link
Contributor

This is intended to help with testing all grpc-based clients. With this, we should be able to mostly avoid using mock/fakes/stubs and instead use this channel-based approach. This means that every layer of the client is tested and there is less concern about a mock/stub/fake not matching its upstream target. This is akin to mocking at the requests level for HTTP clients.

@dhermes: I considered putting this into a package like api_core.testing.grpc_stub, but I decided against it. I'm happy to reconsider.

@googlebot googlebot added the cla: yes This human has signed the Contributor License Agreement. label Jan 5, 2018
if self.response is not None:
return self.response

raise ValueError(

This comment was marked as spam.

This comment was marked as spam.

callable, it will be invoked with the request protobuf. If it's an
exception, the exception will be raised when this is invoked.
"""
self.responses = None

This comment was marked as spam.

This comment was marked as spam.

def __init__(self, method, channel):
self._method = method
self._channel = channel
self.response = None

This comment was marked as spam.

This comment was marked as spam.


@property
def requests(self):
"""Sequence[Tuple[str, protobuf.Message]]: Returns a list of all

This comment was marked as spam.

This comment was marked as spam.

Copy link
Contributor

@lukesneeringer lukesneeringer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A couple relatively minor things, but in general this looks very good to me.

exception, the exception will be raised when this is invoked.
"""
self.responses = None
"""Iterator[protobuf.Message]: A iterator of responses. If specified,

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.



class _CallableStub(object):
"""Stub for the grpc callable interface."""

This comment was marked as spam.

This comment was marked as spam.

Returns:
str: The simplified name of the method.
"""
return method.rsplit('/', 1).pop()

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

request, timeout, metadata, and credentials."""

def __call__(self, request, timeout=None, metadata=None, credentials=None):
self._channel._requests.append((self._method, request))

This comment was marked as spam.

This comment was marked as spam.

self._channel._requests.append((self._method, request))
self.requests.append(request)
self.calls.append(
_ChannelCall(request, timeout, metadata, credentials))

This comment was marked as spam.

This comment was marked as spam.

with pytest.raises(ValueError) as exc_info:
stub.GetOperation(expected_request)

assert exc_info.match('GetOperation')

This comment was marked as spam.

This comment was marked as spam.


response1 = stub.GetOperation(expected_request)
response2 = stub.GetOperation(expected_request)
response3 = stub.GetOperation(expected_request)

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.


def test_subscribe_unsubscribe(self):
channel = grpc_helpers.ChannelStub()
channel.subscribe(None)

This comment was marked as spam.

This comment was marked as spam.

"""Sequence[Tuple[str, protobuf.Message]]: Returns a list of all
requests made on this channel in order. The tuple is of method name,
request proto."""
return self._requests

This comment was marked as spam.

This comment was marked as spam.


def __getattr__(self, key):
# Ideally this would only return stubs for known methods, however,
# grpc doesn't actually create these methods until they're needed.

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

_ChannelCall = collections.namedtuple(
'_ChannelCall', ['request', 'timeout', 'metadata', 'credentials'])
_MethodCall = collections.namedtuple(
'_MethodCall', ['request', 'timeout', 'metadata', 'credentials'])

This comment was marked as spam.

This comment was marked as spam.

if self.responses:
self.response = next(self.responses)
response = self.response
if response is None and self.responses is not None:

This comment was marked as spam.

This comment was marked as spam.

channel_stub.GetFoo.responses = iter([
foo_pb2.Foo(name='bar'),
foo_pb2.Foo(name='baz')])
foo_pb2.Foo(name='baz')

This comment was marked as spam.

This comment was marked as spam.

try:
return self._method_stubs[key]
except KeyError:
raise AttributeError

This comment was marked as spam.

def subscribe(self, callback, try_to_connect=False):
"""grpc.Channel.subscribe implementation."""
return
pass

This comment was marked as spam.

This comment was marked as spam.

def on_get_operation(request):
return expected_response
on_get_operation = mock.Mock(
spec=('__call__'), return_value=expected_response)

This comment was marked as spam.

This comment was marked as spam.

Copy link
Contributor

@dhermes dhermes left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@theacodes theacodes merged commit e9a5347 into master Jan 5, 2018
@theacodes theacodes deleted the channel-mock branch January 5, 2018 22:54
@theacodes
Copy link
Contributor Author

Thanks, @dhermes and @lukesneeringer!

@dhermes, @tseaver, @chemelnucfin, @lukesneeringer: Please keep an eye out in code reviews for excessive mocking of gapic/grpc clients - where possible, this should be used instead. I'm happy to help write tests until we develop a good reference corpus.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

api: core cla: yes This human has signed the Contributor License Agreement. testing

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants