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