Mocking Stripe with Rspec
Motivation
There are a many ways to deal with testing integrations. In the past I have used VCR with great success, specially when external responses are complex JSON or XML structures.
In my latest project I am integrating the wonderful Stripe API and I wanted to throughly test that. I decided not to use VCR because it would be time consuming to craft all those real fail and success responses and record them.
My first stop was Stripe ruby mock gem which integrates nicely with RSpec. Unfortunately that is not compatible with Stripe's 2.0 API and it looks like it's not going to be trivial to update the gem.
My solution was to use RSpec Mocks instead. It's not very pretty but it works.
The code
Let's say I'm trying to test the deletion of a plan using the Stripe::Plan
class. Not that this code is just for illustration purposes.
def destroy
# I have an internal plan model with a stripe_id column.
@plan = Plan.find(params[:id])
begin
stripe_plan = Stripe::Plan.retrieve(@plan.stripe_id)
stripe_plan.delete
@plan.destroy
redirect_to plans_path, notice: 'Plan was successfully deleted.'
rescue Stripe::StripeError => e
@plan.errors[:base] << e.message
redirect_to plans_path, notice: @plan.errors
end
end
If there's an error deleting the Stripe plan I don't want to destroy my internal plan.
The spec
The deletion is a two step process: first retrieve and then call the delete instance method. Testing this in the rails console you can see that Stripe returns an instance of Stripe::Plan when you call the retrieve method.
irb(main):001:0> Stripe::Plan.retrieve('plan_id')
=> #<Stripe::Plan:0x3fc7a72262b8 id=plan_id> JSON: {
"id": "plan_id",
"object": "plan",
"amount": 3000,
"created": 1490967332,
"currency": "cad",
"interval": "day",
"interval_count": 1,
"livemode": false,
"metadata": {},
"name": "My plan",
"statement_descriptor": null,
"trial_period_days": null
}
irb(main):002:0>
Also, calling Stripe::Plan.new
conveniently returns an empty instance. This means I can mock the retrieve class method to return an instance and then mock the delete method of any instance of that.
describe 'DELETE #destroy' do
it 'returns an error when Stripe::StripeError is raise' do
allow(Stripe::Plan).to receive(:retrieve).and_return(Stripe::Plan.new)
allow_any_instance_of(Stripe::Plan).to receive(:delete).and_raise(Stripe::StripeError.new('Mock error message'))
delete :destroy, params: { id: plan.id }
expect(assigns(:plan).errors.full_messages).to match_array(['Mock error message'])
end
end