protobuf-rspec gem
Provides spec helpers for testing client and server protobuf code.
RSpec Helpers are designed to give you mock abstraction of client or service layer. Require as protobuf/rspec and include into your running RSpec configuration.
# spec_helper.rb
# ...
require 'protobuf/rspec'
RSpec.configure do |config|
config.include Protobuf::RSpec::Helpers
endUnit-Testing Service Behavior
local_rpc
To unit test your service you should use the local_rpc helper method. local_rpc helps you call the service instance method of your choosing to ensure that the correct responses are generated with the given requests. This should be used to outside-in test a local RPC Service without testing the underlying socket implementation or needing actual client code to invoke the endpoint method under test. Any filters added to the service will be invoked.
Given the service implementation below:
module Services
class UserService < Protobuf::Rpc::Service
def create
if request.name
user = User.create_from_proto(request)
respond_with(user)
else
rpc_failed 'Error: name required'
end
end
def notify
user = User.find_by_guid(request.guid)
if user
Resque.enqueue(EmailUserJob, user.id)
respond_with(:queued => true)
else
rpc_failed 'Error: user not found'
end
end
end
endSpecs that test these two methods and their various cases could look something like this:
describe Services::UserService do
describe '#create' do
subject { local_rpc(:create, request) }
context 'when request is valid' do
let(:request) { { :name => 'Jack' } }
let(:user_mock) { FactoryGirl.build(:user) }
before { User.should_receive(:create_from_proto).and_return(user_mock) }
it { should eq(user_mock) }
end
context 'when name is not given' do
let(:request) { :name => '' }
it { should =~ /Error/ }
end
end
describe '#notify' do
let(:request) { { :guid => 'USR-123' } }
let(:user_mock) { FactoryGirl.build(:user) }
subject { local_rpc(:notify, request) }
context 'when user is found' do
before { User.should_receive(:find_by_guid).with(request.guid).and_return(user_mock) }
before { Resqueue.should_receive(:enqueue).with(EmailUserJob, request.guid)
its(:queued) { should be_true }
end
context 'when user is not found' do
before { Resque.should_not_receive(:enqueue) }
it { should =~ /Error/ }
end
end
endrpc
Make an RPC call (without testing the underlying socket implementation). Works the same as local_rpc, but invokes the entire RPC middleware stack (service filters are also run):
rpc(:create, user_request) # => UserService#createrpc_env
Initialize a new RPC env object simulating what happens in the middleware stack.
Useful for testing a service class directly without using rpc or local_rpc.
describe "#create" do
# Initialize request and response
# ...
let(:env) { rpc_env(:create, request) }
subject { described_class.new(env) }
it "creates a user" do
subject.create
subject.response.should eq response
end
endsubject_service
One thing to note is that local_rpc uses described_class as the class to invoke for the given method. If you need to instead test a different class than your described_class, simply pass a block to subject_service which returns the class you would like to use instead.
describe 'The User Service' do
subject_service { Services::UserService }
describe '#create' do
subject { local_rpc(:create, request) }
# ...
end
#...
end
request_class and response_class
Both the request_class and response_class helper methods will return the class type for, you guessed it, the request and response type defined by the service method. This can aid in setting up the correct objects for expectations. Simply pass in the name of the endpoint you are testing to get the appropriate message class.
request_class(:create) # => UserCreateRequest
response_class(:create) # => UserMocking Service Responses
Create a mock service that responds in the way you are expecting to aid in testing client -> service calls. In order to test your success callback you should provide a :success option. To test your failure callback you should provide a :failure option.
Testing the client on_success callback
Passing a :success key as an option to mock_rpc will cause the on_success callback to be invoked with the given object. In this way you can simulate a successful service response to verify that you are handling the response appropriately. You can alternatively use the :response key to invoke the on_success block.
# Method under test
def create_user(request)
status = 'unknown'
Proto::UserService.client.create(request) do |c|
c.on_success do |response|
status = response.status
end
end
status
end
...
# spec
it 'verifies the on_success method behaves correctly' do
response_mock = mock('response_mock', :status => 'success')
mock_rpc(Proto::UserService, :client, :success => response_mock) # alternatively can use :response key here
create_user(request).should eq('success')
endTesting the client on_failure callback
Passing a :failure key as an option to mock_rpc will cause the on_failure callback to be invoked with the given object. In this way you can simulate a service failure and verify you are handling that failure appropriately. You can alternatively use the :error key to invoke the on_failure block.
# Method under test
def create_user(request)
status = nil
Proto::UserService.client.create(request) do |c|
c.on_failure do |error|
status = 'error'
ErrorReporter.report(error.message)
end
end
status
end
...
# spec
it 'verifies the on_success method behaves correctly' do
error_mock = mock('error_mock', :message => 'this is an error message')
mock_rpc(Proto::UserService, :client, :failure => error_mock) # alternatively can use :error key here
ErrorReporter.should_receive(:report).with(error_mock.message)
create_user(request).should eq('error')
endTesting the given client request object (direct assert)
In order to test the request object sent to the service you can pass a :request key whose value will be asserted with RSpec's with constraint paired with the should_receive assertion. Also note that if a :request option is given, the assert block will be ignored (see below).
# Method under test
def create_user
request = ... # some operation to build a request on state
Proto::UserService.client.create(request) do |c|
...
end
end
...
# spec
it 'verifies the request is built correctly' do
expected_request = ... # some expectation
mock_rpc(Proto::UserService, :client, :request => expected_request)
create_user(request)
endTesting the given client request object (block assert)
You can also pass a block to mock_rpc which will be yielded the request object. This allows more fine-grained assertions on the request object. Also note that if a :request option is given (see above), the assert block will be ignored.
# Method under test
def create_user
request = ... # some operation to build a request on state
Proto::UserService.client.create(request) do |c|
...
end
end
...
# spec
it 'verifies the request is built correctly' do
mock_rpc(Proto::UserService, :client) do |given_request|
given_request.field1.should eq 'rainbows'
given_request.field2.should eq 'ponies'
end
create_user(request)
endFeedback
Feedback and comments are welcome:
Twitter: @localshred Github: github