Skip to content

Commit

Permalink
Merge pull request ganmacs#2 from ganmacs/support-stub_request
Browse files Browse the repository at this point in the history
Support stub request
  • Loading branch information
ganmacs authored Jan 7, 2019
2 parents 0f477a8 + 5e1aedf commit eeeb927
Show file tree
Hide file tree
Showing 21 changed files with 710 additions and 27 deletions.
45 changes: 38 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
# GrpcMock [![Build Status](https://travis-ci.org/ganmacs/grpc_mock.svg?branch=master)](https://travis-ci.org/ganmacs/grpc_mock)

Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/grpc_mock`. To experiment with that code, run `bin/console` for an interactive prompt.

TODO: Delete this and the text above, and describe your gem
Library for stubbing grpc in Ruby.

## Installation

Expand All @@ -22,13 +20,46 @@ Or install it yourself as:

## Usage

TODO: Write usage instructions here
If you use [RSpec](https://github.com/rspec/rspec), add the following code to spec/spec_helper.rb:

```ruby
require 'grpc_mock/rspec'
```

## Examples

## Development
See definition of protocol buffers and gRPC generated code in [spec/exmaples/hello](https://github.com/ganmacs/grpc_mock/tree/master/spec/examples/hello)

##### Stubbed request based on path and with the default response

```ruby
GrpcMock.stub_request("/hello.hello/Hello").to_return(Hello::HelloResponse.new(msg: 'test'))

client = Hello::Hello::Stub.new('localhost:8000', :this_channel_is_insecure)
client = client.hello(Hello::HelloRequest.new(msg: 'hi')) # => Hello::HelloResponse.new(msg: 'test')
```

After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
##### Stubbing requests based on path and request

To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
```ruby
GrpcMock.stub_request("/hello.hello/Hello").with(Hello::HelloRequest.new(msg: 'hi')).to_return(Hello::HelloResponse.new(msg: 'test'))

client = Hello::Hello::Stub.new('localhost:8000', :this_channel_is_insecure)
client = client.hello(Hello::HelloRequest.new(msg: 'hello')) # => send a request to server
client = client.hello(Hello::HelloRequest.new(msg: 'hi')) # => Hello::HelloResponse.new(msg: 'test') (without any requests to server)
```

##### Real requests to network can be allowed or disabled

```ruby
client = Hello::Hello::Stub.new('localhost:8000', :this_channel_is_insecure)

GrpcMock.disable_net_connect!
client = client.hello(Hello::HelloRequest.new(msg: 'hello')) # => Raise NetConnectNotAllowedError error

GrpcMock.allow_net_connect!
client = Hello::Hello::Stub.new('localhost:8000', :this_channel_is_insecure) # => send a request to server
```

## Contributing

Expand Down
12 changes: 8 additions & 4 deletions lib/grpc_mock.rb
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
require 'grpc_mock/api'
require 'grpc_mock/version'
require 'grpc_mock/configuration'
require 'grpc_mock/adapter'
require 'grpc_mock/stub_registry'

module GrpcMock
extend GrpcMock::Api

class << self
def enable!
adapter.enable!
Expand All @@ -12,12 +16,12 @@ def disable!
adapter.disable!
end

def disable_net_connect!
config.allow_net_connect = false
def reset!
GrpcMock.stub_registry.reset!
end

def allow_net_connect!
config.allow_net_connect = true
def stub_registry
@stub_registry ||= GrpcMock::StubRegistry.new
end

def adapter
Expand Down
24 changes: 24 additions & 0 deletions lib/grpc_mock/api.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
require 'grpc_mock/request_stub'
require 'grpc_mock/matchers/request_including_matcher'

module GrpcMock
module Api
# @param path [String]
def stub_request(path)
GrpcMock.stub_registry.register_request_stub(GrpcMock::RequestStub.new(path))
end

# @param values [Hash]
def request_including(values)
GrpcMock::Matchers::RequestIncludingMatcher.new(values)
end

def disable_net_connect!
GrpcMock.config.allow_net_connect = false
end

def allow_net_connect!
GrpcMock.config.allow_net_connect = true
end
end
end
6 changes: 6 additions & 0 deletions lib/grpc_mock/errors.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,10 @@ def initialize(sigunature)
super("Real GRPC connections are disabled. #{sigunature} is requested")
end
end

class NoResponseError < StandardError
def initialize(msg)
super("There is no response: #{msg}")
end
end
end
31 changes: 23 additions & 8 deletions lib/grpc_mock/grpc_stub_adapter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,48 +6,63 @@ class GrpcStubAdapter
# To make hook point for GRPC::ClientStub
# https://github.com/grpc/grpc/blob/bec3b5ada2c5e5d782dff0b7b5018df646b65cb0/src/ruby/lib/grpc/generic/service.rb#L150-L186
class AdapterClass < GRPC::ClientStub
def request_response(method, *args)
def request_response(method, request, *args)
unless GrpcMock::GrpcStubAdapter.enabled?
return super
end

if GrpcMock.config.allow_net_connect
mock = GrpcMock.stub_registry.response_for_request(method, request)
if mock
mock
elsif GrpcMock.config.allow_net_connect
super
else
raise NetConnectNotAllowedError, method
end
end

def client_streamer(method, *args)
# TODO
def client_streamer(method, requests, *args)
unless GrpcMock::GrpcStubAdapter.enabled?
return super
end

if GrpcMock.config.allow_net_connect
r = requests.to_a # FIXME: this may not work
mock = GrpcMock.stub_registry.response_for_request(method, r)
if mock
mock
elsif GrpcMock.config.allow_net_connect
super
else
raise NetConnectNotAllowedError, method
end
end

def server_streamer(method, *args)
def server_streamer(method, request, *args)
unless GrpcMock::GrpcStubAdapter.enabled?
return super
end

if GrpcMock.config.allow_net_connect
mock = GrpcMock.stub_registry.response_for_request(method, request)
if mock
mock
elsif GrpcMock.config.allow_net_connect
super
else
raise NetConnectNotAllowedError, method
end
end

def bidi_streamer(method, *args)
def bidi_streamer(method, requests, *args)
unless GrpcMock::GrpcStubAdapter.enabled?
return super
end

if GrpcMock.config.allow_net_connect
r = requests.to_a # FIXME: this may not work
mock = GrpcMock.stub_registry.response_for_request(method, r)
if mock
mock
elsif GrpcMock.config.allow_net_connect
super
else
raise NetConnectNotAllowedError, method
Expand Down
41 changes: 41 additions & 0 deletions lib/grpc_mock/matchers/hash_argument_matcher.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
module GrpcMock
module Matchers
# Base class for Hash matchers
# https://github.com/rspec/rspec-mocks/blob/master/lib/rspec/mocks/argument_matchers.rb
class HashArgumentMatcher
def self.stringify_keys!(arg, options = {})
case arg
when Array
arg.map { |elem|
options[:deep] ? stringify_keys!(elem, options) : elem
}
when Hash
Hash[
*arg.map { |key, value|
k = key.is_a?(Symbol) ? key.to_s : key
v = (options[:deep] ? stringify_keys!(value, options) : value)
[k,v]
}.inject([]) {|r,x| r + x}]
else
arg
end
end

def initialize(expected)
@expected = Hash[
GrpcMock::Matchers::HashArgumentMatcher.stringify_keys!(expected, deep: true).sort,
]
end

def ==(_actual, &block)
@expected.all?(&block)
rescue NoMethodError
false
end

def self.from_rspec_matcher(matcher)
new(matcher.instance_variable_get(:@expected))
end
end
end
end
37 changes: 37 additions & 0 deletions lib/grpc_mock/matchers/request_including_matcher.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
require 'grpc_mock/matchers/hash_argument_matcher'

module GrpcMock
module Matchers
class RequestIncludingMatcher < HashArgumentMatcher
def ==(actual)
if actual.respond_to?(:to_h)
actual = actual.to_h
end

actual = Hash[GrpcMock::Matchers::HashArgumentMatcher.stringify_keys!(actual, deep: true)]
super { |key, value| inner_including(value, key, actual) }
rescue NoMethodError
false
end

private

def inner_including(expect, key, actual)
if actual.key?(key)
actual_value = actual[key]
if expect.is_a?(Hash)
RequestIncludingMatcher.new(expect) == actual_value
else
expect === actual_value
end
else
false
end
end

def inspect
"reqeust_including(#{@expected.inspect})"
end
end
end
end
23 changes: 23 additions & 0 deletions lib/grpc_mock/request_pattern.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
module GrpcMock
class RequestPattern
# @param path [String]
def initialize(path)
@path = path
@block = nil
@request = nil
end

def with(request = nil, &block)
if request.nil? && !block_given?
raise ArgumentError, '#with method invoked with no arguments. Either options request or block must be specified.'
end

@request = request
@block = block
end

def match?(path, request)
@path == path && (@request.nil? || @request == request) && (@block.nil? || @block.call(path))
end
end
end
44 changes: 44 additions & 0 deletions lib/grpc_mock/request_stub.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
require 'grpc_mock/request_pattern'
require 'grpc_mock/resopnse_sequence'
require 'grpc_mock/errors'

module GrpcMock
class RequestStub
# @param path [String] gRPC path like /${service_name}/${method_name}
def initialize(path)
@request_pattern = RequestPattern.new(path)
@response_sequence = []
end

def with(request = nil, &block)
@request_pattern.with(request, &block)
self
end

def to_return(*resp)
@response_sequence << GrpcMock::ResponsesSequence.new(resp)
self
end

def response
if @response_sequence.empty?
raise GrpcMock::NoResponseError, 'Must be set some values by using #GrpMock::RequestStub#to_run'
elsif @response_sequence.size == 1
@response_sequence.first.next
else
if @response_sequence.first.end?
@response_sequence.shift
end

@response_sequence.first.next
end
end

# @param path [String]
# @param request [Object]
# @return [Bool]
def match?(path, request)
@request_pattern.match?(path, request)
end
end
end
37 changes: 37 additions & 0 deletions lib/grpc_mock/resopnse_sequence.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
module GrpcMock
class ResponsesSequence
attr_accessor :repeat

def initialize(responses)
@repeat = 1
@responses = responses
@current = 0
@last = @responses.length - 1
end

def end?
@repeat == 0
end

def next
if @repeat > 0
response = @responses[@current]
next_pos
response
else
@responses.last
end
end

private

def next_pos
if @last == @current
@current = 0
@repeat -= 1
else
@current += 1
end
end
end
end
4 changes: 4 additions & 0 deletions lib/grpc_mock/rspec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,8 @@
config.after(:suite) do
GrpcMock.disable!
end

config.after(:each) do
GrpcMock.reset!
end
end
Loading

0 comments on commit eeeb927

Please sign in to comment.