Where and how to expect on Rspec / VCR stubbed response? - ruby

I'm writing a client in ruby for an API that we're using. We use rspec and VCR with webmock to mock request/responses to the API.
What is the best or appropriate way to test the response back from the API when the response payload is really large?
Is there a best practice around where to put the large expected payload and how to test this?
require 'spec_helper'
describe Service::API, vcr: true do
describe '.method' do
it 'returns valid response' do
#returns large body payload
response = subject.method
expect(response).to eq ???
end
end
end

You shouldn't be testing the payload, you need to test that the method does what you expect with that payload. VCR is going to take care of storing the payload. You may need to assert that you send what you expect to the API, and assert what you do with the result. You should also test the fail scenarios, like the API times out, or network error etc. But the payload itself you shouldn't really need to touch in the test.
You will probably find that it helps to break the test down into scenarios;
given a call with the correct params
given a bad call e.g. missing resource at the API end
given a network error
I tend to find it easier to just write some of the obvious scenarios and then start from the assert for each one. Something like below, somewhat abridged and using RSpec but you should get the idea;
describe Transaction do
# Create a real object in the api.
let(:transaction) { create :transaction }
describe '.find', :vcr do
context 'given a valid id' do
subject { Transaction.find(transaction.id) }
it 'returns a Transaction object' do
is_expected.to eq(transaction)
end
end
context 'given an invalid transaction_id' do
subject { Transaction.find('my-bad-transaction') }
it 'should rescue Gateway::NotFoundError' do
is_expected.to be_nil
end
it 'raises no NotFoundError' do
expect { subject }.to raise_error(Gateway::NotFoundError)
end
end
end
end

Related

How do you test custom Bugsnag meta_data in Ruby?

How do you test custom Bugsnag meta_data (in Ruby, with Rspec)?
The code that I want to test:
def do_something
thing_that_could_error
rescue => e
Bugsnag.notify(e) do |r|
r.meta_data = { my_extra_data: "useful info" }
end
end
The test I want to write:
context "when there's an error" do
it "calls Bugsnag with my special metadata" do
expect(Bugsnag).to receive(:notify) # TODO test meta_data values contain "my useful info"
expect do
do_something() # exception is thrown and rescued and sent to Bugsnag
end.not_to raise_error
end
end
I am using:
Ruby 2.6.6
Rspec 3.9.0
Bugsnag 6.17.0 https://rubygems.org/gems/bugsnag
The data inside of the meta_data variable is considerably more complicated than in this tiny example, which is why I want to test it. In a beautiful world, I would extract that logic to a helper and test the helper, but right now it is urgent and useful to test in situ.
I've been looking at the inside of the Bugsnag gem to figure this out (plus some Rspec-fu to capture various internal state and returned data) but at some point it's a good idea to ask the internet.
Since the metadata is complicated, I'd suggest simplifying it:
def do_something
thing_that_could_error
rescue => e
Bugsnag.notify(e) do |r|
r.meta_data = error_metadata(e, self, 'foo')
end
end
# I assumed that you'd like to pass exception and all the context
def error_metadata(e, object, *rest)
BugsnagMetadataComposer.new(e, object, *rest).metadata
end
So now you can have a separate test for BugsnagMetadataComposer where you have full control (without mocking) over how you initialize it, and test for metadata output.
Now you only have to test that BugsnagMetadataComposer is instantiated with the objects you want, metadata is called and it returns dummy hash:
let(:my_exception) { StandardError.new }
let(:mock_metadata) { Hash.new }
before do
# ensure thing_that_could_error throws `my_exception`
expect(BugsnagMetadataComposer)
.to receive(new)
.with(my_exception, subject, anything)
.and_return(mock_metadata)
end
And the hard part, ensure that metadata is assigned. To do that you can cheat a little and see how Bugsnag gem is doing it
Apparently there's something called breadcrumbs:
let(:breadcrumbs) { Bugsnag.configuration.breadcrumbs }
Which I guess has all the Bugsnag requests, last one on top, so you can do something similar to https://github.com/bugsnag/bugsnag-ruby/blob/f9c539670c448f7f129a3f8be7d412e2e824a357/spec/bugsnag_spec.rb#L36-L40
specify do
do_something()
expect(breadcrumbs.last.metadata).to eq(expected_metadata)
end
And for clarity, the whole spec would look a bit like this:
let(:my_exception) { StandardError.new }
let(:mock_metadata) { Hash.new }
before do
# ensure thing_that_could_error throws `my_exception`
expect(BugsnagMetadataComposer)
.to receive(new)
.with(my_exception, subject, anything)
.and_return(mock_metadata)
end
specify do
do_something()
expect(breadcrumbs.last.metadata).to eq(expected_metadata)
end

How to test error handling with RSpec?

I try to test this code:
mail = Mail.new
mail.from = #from
mail.to = to
<...>
begin
mail.deliver
rescue
return false
else
return true
end
for error handling with RSpec:
it 'should return true if success' do
expect(mailer.send_confirmation_request(token)).to be true
end
it 'should return false if fails' do
allow_any_instance_of(Mail).to receive(:deliver).and_raise('Mail error')
expect(mailer.send_confirmation_request(token)).to be false
end
But it doesn't work: second test failed. Why does allow_any_instance_of (and allow(Mail)) not work in this way? How that can be tested? Should am I fix my code to be more testable? I can test it by stubbing Mail#new with rspec double, but it leads to stubbing all mail methods and not looks like a right way to do.
If it does matter, I use mikel's mail gem and make tests according to his recommendations.
Mail.new doesn't return an instance of Mail, it returns an instance of Mail::Message, so you're calling allow_any_instance_of on the wrong class.
I assume the library does this to simplify the interface, but it is a little counterintuitive.
When you mock or stub a method using expect_any_instance_ orallow_any_instance_`, you're actually replacing the normal value of that method with what you return in the stub.
You can use and_call_original if you want to preserve the original implementation of the method call.
Relevant RSpec docs:
https://www.relishapp.com/rspec/rspec-mocks/v/2-14/docs/message-expectations/calling-the-original-method

Rspec Ruby Mocking

I would like to achieve 100% coverage on a module. My problem is that there is a variable (called data) within a method which I am trying to inject data in to test my exception handling. Can this be done with mocking? If not how can i fully test my exception handling?
module CSV
module Extractor
class ConversionError < RuntimeError; end
class MalformedCSVError < RuntimeError; end
class GenericParseError < RuntimeError; end
class DemoModeError < RuntimeError; end
def self.open(path)
data = `.\\csv2text.exe #{path} -f xml --xml_output_styles 2>&1`
case data
when /Error: Wrong input filename or path:/
raise MalformedCSVError, "the CSV path with filename '#{path}' is malformed"
when /Error: A valid password is required to open/
raise ConversionError, "Wrong password: '#{path}'"
when /CSVTron CSV2Text: This page is skipped when running in the demo mode./
raise DemoModeError, "CSV2TEXT.exe in demo mode"
when /Error:/
raise GenericParseError, "Generic Error Catch while reading input file"
else
begin
csvObj = CSV::Extractor::Document.new(data)
rescue
csvObj = nil
end
return csvObj
end
end
end
end
Let me know what you think! Thanks
===================== EDIT ========================
I have modified my methods to the design pattern you suggested. This method-"open(path)" is responsible for trapping and raising errors, get_data(path) just returns data, That's it! But unfortunately in the rspec I am getting "exception was expected to be raise but nothing was raised." I thought maybe we have to call the open method from your stub too?
This is what I tried doing but still no error was raised..
it 'should catch wrong path mode' do
obj = double(CSV::Extractor)
obj.stub!(:get_data).and_return("Error: Wrong input filename or path:")
obj.stub!(:open)
expect {obj.open("some fake path")}.to raise_error CSV::Extractor::MalformedCSVError
end
Extract the code that returns the data to a separate method. Then when you test open you can stub out that method to return various strings that will exercise the different branches of the case statement. Roughly like this for the setup:
def self.get_data(path)
`.\\csv2text.exe #{path} -f xml --xml_output_styles 2>&1`
end
def self.open(path)
data = get_data(path)
...
And I assume you know how to stub methods in rspec, but the general idea is like this:
foo = ...
foo.stub(:get_data).and_return("Error: Wrong input filename or path:")
expect { foo.get_data() }.to raise_error MalformedCSVError
Also see the Rspec documentation on testing for exceptions.
Problem with testing your module lies in the way you have designed your code. Think about splitting extractor into two classes (or modules, it's matter of taste -- I'd go with classes as they are a bit easier to test), of which one would read data from external system call, and second would expect this data to be passed as an argument.
This way you can easily mock what you currently have in data variable, as this would be simply passed as an argument (no need to think about implementation details here!).
For easier usage you can later provide some wrapper call, that would create both objects and pass one as argument to another. Please note, that this behavior can also be easily tested.

Using specific VCR cassette based on request

Situation: testing a rails application using Rspec, FactoryGirl and VCR.
Every time a User is created, an associated Stripe customer is created through Stripe's API. While testing, it doesn't really makes sense to add a VCR.use_cassette or describe "...", vcr: {cassette_name: 'stripe-customer'} do ... to every spec where User creation is involved. My actual solution is the following:
RSpec.configure do |config|
config.around do |example|
VCR.use_cassette('stripe-customer') do |cassette|
example.run
end
end
end
But this isn't sustainable because the same cassette will be used for every http request, which of course is very bad.
Question: How can I use specific fixtures (cassettes) based on individual request, without specifying the cassette for every spec?
I have something like this in mind, pseudo-code:
stub_request(:post, "api.stripe.com/customers").with(File.read("cassettes/stripe-customer"))
Relevant pieces of code (as a gist):
# user_observer.rb
class UserObserver < ActiveRecord::Observer
def after_create(user)
user.create_profile!
begin
customer = Stripe::Customer.create(
email: user.email,
plan: 'default'
)
user.stripe_customer_id = customer.id
user.save!
rescue Stripe::InvalidRequestError => e
raise e
end
end
end
# vcr.rb
require 'vcr'
VCR.configure do |config|
config.default_cassette_options = { record: :once, re_record_interval: 1.day }
config.cassette_library_dir = 'spec/fixtures/cassettes'
config.hook_into :webmock
config.configure_rspec_metadata!
end
# user_spec.rb
describe :InstanceMethods do
let(:user) { FactoryGirl.create(:user) }
describe "#flexible_name" do
it "returns the name when name is specified" do
user.profile.first_name = "Foo"
user.profile.last_name = "Bar"
user.flexible_name.should eq("Foo Bar")
end
end
end
Edit
I ended doing something like this:
VCR.configure do |vcr|
vcr.around_http_request do |request|
if request.uri =~ /api.stripe.com/
uri = URI(request.uri)
name = "#{[uri.host, uri.path, request.method].join('/')}"
VCR.use_cassette(name, &request)
elsif request.uri =~ /twitter.com/
VCR.use_cassette('twitter', &request)
else
end
end
end
VCR 2.x includes a feature specifically to support use cases like these:
https://relishapp.com/vcr/vcr/v/2-4-0/docs/hooks/before-http-request-hook!
https://relishapp.com/vcr/vcr/v/2-4-0/docs/hooks/after-http-request-hook!
https://relishapp.com/vcr/vcr/v/2-4-0/docs/hooks/around-http-request-hook!
VCR.configure do |vcr|
vcr.around_http_request(lambda { |req| req.uri =~ /api.stripe.com/ }) do |request|
VCR.use_cassette(request.uri, &request)
end
end
IMO, libraries like this should provided you with a mock class, but w/e.
You can do your pseudocode example already with Webmock, which is the default internet mocking library that VCR uses.
body = YAML.load(File.read 'cassettes/stripe-customer.yml')['http_interactions'][0]['response']['body']['string']
stub_request(:post, "api.stripe.com/customers").to_return(:body => body)
You could put that in a before block that only runs on a certain tag, then tag the requests that make API calls.
In their tests, they override the methods that delegate to RestClient (link). You could do this as well, take a look at their test suite to see how they use it, in particular their use of test_response. I think this is a terribly hacky way of doing things, and would feel really uncomfortable with it (note that I'm in the minority with this discomfort) but it should work for now (it has the potential to break without you knowing until runtime). If I were to do this, I'd want to build out real objects for the two mocks (the one mocking rest-client, and the other mocking the rest-client response).
The whole point (mostly anyway) of VCR to just to replay the response of a previous request. If you are in there picking and choosing what response goes back to what request, you are quote/unquote doing-it-wrong.
Like Joshua already said, you should use Webmock for something like this. That's what VCR is uing behind the scenes anyway.

RSpec and Open-URI how do I mock raise a SocketError/TimeoutError

I want to be able to spec out that when Open-Uri open() calls either timeout or raise an exception such as SocketError I am handling things as expected, however I'm having trouble with this.
Here is my spec (for SocketError):
#obj.should_receive(:open).with("some_url").and_raise(SocketError)
And the part of my object where I'm using open-uri:
begin
resp = open(url)
resp = resp.read
rescue SocketError
something = true
end
However in this situation the spec fails as with a nil.read error.
This is the second time this week I've come across this problem, the previous time I was attempting to simulate a TimeoutError when wrapping open() with a timeout() {}, that time I gave up and just caused an actual timeout to happen by opening up the class. I could obviously cause this to throw a SocketError by trying to call an invalid URL, but I'm sure there is a correct way to mock this out with RSpec.
Update: I obviously wasn't thinking clearly that late at night, the error was actually when I re-tried the URL after the SocketError, the and_raise(SocketError) part worked fine.
The line you provided should work, based on the information you've given: I made a tiny test class and spec (see below) with only the described functionality, and things behaved as expected. It might be helpful if you could provide a little more context - the full "it" block from the spec, for instance, might expose some other problem.
As mentioned, the following spec passes, and I believe it captures the logic you were attempting to verify:
require 'rubygems'
require 'spec'
class Foo
attr_accessor :socket_error
def get(url)
#socket_error = false
begin
resp = open(url)
resp = resp.read
rescue SocketError
#socket_error = true
end
end
end
describe Foo do
before do
#foo = Foo.new
end
it "should handle socket errors" do
#foo.should_receive(:open).with("http://www.google.com").and_raise(SocketError)
#foo.get("http://www.google.com")
#foo.socket_error.should be_true
end
end

Resources