Skip to content

Instantly share code, notes, and snippets.

@pat
Created January 9, 2016 07:28
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save pat/829156ebfe4f7c638a31 to your computer and use it in GitHub Desktop.
Save pat/829156ebfe4f7c638a31 to your computer and use it in GitHub Desktop.

I've written this code to test what happens once a Stripe card token has been passed through to the server. This code does not do any browser testing (because JS-friendly headless testing is painful at best, and impossible if you want VCR in the mix). VCR is being used here to capture interactions with a test Stripe account, to ensure the tests are fast on future runs. That said, it's structured so that nothing is expected to be present in the Stripe account. If you delete all the stored VCR cassettes, the specs will safely re-record them.

Instead of using VCR.use_cassette directly, instead you use stripe_cassette in your specs, passing in the current spec example object. This will automatically generate a cassette name based on the spec's file name and the example's description (limitations of this are noted in the code comments). Within the cassette's recording block, Stripe data is cleared out before yielding back to the spec, with a context object. The context object provides access to helper methods - currently there's just one, for generating card tokens - and these helpers can be aware of the VCR cassette's context (i.e. is it actually recording, or just replaying).

class StripeCassette
def self.call(example, &block)
new(example, &block).call
end
def initialize(example, &block)
@example, @block = example, block
end
def call
# Automatically generate a cassette name based on the name
# of the spec file, and the description of this example. That
# does mean if either of those things change, the VCR file will
# not align - but you're aware of that now, right? So you'll
# move things accordingly.
# That said, this code doesn't take into account nested
# describe/context blocks. Adapt as you see fit.
VCR.use_cassette("#{folder}/#{file}") do |cassette|
# Clear out all existing Stripe data. We want a clean slate.
clear
# This app I'm working on expects a plan to be present from the
# very beginning, so lets create it. If this code was to become
# a gem, we'd need some sort of hook here instead.
create_plan
block.call StripeContext.new(cassette)
end
end
private
attr_reader :example, :block
def clear
Stripe::Customer.all.each &:delete
Stripe::Coupon.all.each &:delete
Stripe::Plan.all.each &:delete
end
def create_plan
Stripe::Plan.create(
:id => ENV['STRIPE_PLAN_ID'],
:amount => 3_00,
:currency => 'USD',
:interval => 'month',
:interval_count => 1,
:name => 'Drumknott Test'
)
end
def file
example.metadata[:description].downcase.gsub(/\s+/, '_').gsub(/[\W]+/, '')
end
def folder
File.basename example.metadata[:file_path], '_spec.rb'
end
end
class StripeContext
def initialize(cassette)
@cassette = cassette
end
# Get a card token for the given credit card value.
# If the cassette isn't actually being recorded, then we don't
# need a real value.
def card_token(card = '4242424242424242')
return 'tok_non_live_example' unless cassette.recording?
Stripe::Token.create(:card => {
:number => card,
:exp_month => 1,
:exp_year => (Time.current.year + 2),
:cvc => '123'
}).id
end
private
attr_reader :cassette
end
module StripeHelpers
def stripe_cassette(example, &block)
StripeCassette.call example, &block
end
end
RSpec.configure do |config|
config.include StripeHelpers
end
require 'rails_helper'
RSpec.describe 'Creating a new site subscription' do
let(:site) { Site.make! }
# Make sure you yield with the example object - we need it.
it 'creates a new Stripe subscription' do |example|
# Instead of using VCR directly, let's use our new helper method.
# The yielded object here isn't actually the VCR cassette, but the
# StripeContext wrapper.
stripe_cassette(example) do |cassette|
# This is what gets called in the controller. It'd be nice if this
# was tested via Capybara, but I'm using Stripe's Checkout button,
# and I don't want to deal with Capybara, JS, and VCR.
SubscribeSiteWorker.perform_async site.id, cassette.card_token
site.reload
customer = Stripe::Customer.retrieve site.user.stripe_customer_id
subscription = customer.subscriptions.retrieve site.stripe_subscription_id
expect(subscription).to be_present
expect(subscription.plan.id).to eq(ENV['STRIPE_PLAN_ID'])
expect(site.status).to eq('active')
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment