Remove stripe-ruby-mock gem
This post is a part of YAMAP Engineers Advent Calendar 2023.
Introduction
Stripe is an excellent payment service with very nice developer experience. They provide extensive support for testing their services in test mode. However, like other third-party services, automated testing can be a bit challenging.
A good option is stripe-ruby-mock gem. Simple add the gem, and you're good to go - it works out-of-the box, making test writing a breeze.
However, it does come with a couple of drawbacks:
- It locks the Stripe gem version, limiting your ability to keep your production gem version up to date, which doesn't feel right for a test gem.
- It mocks the Stripe server, but given Stripe's frequent updates, there's a risk of not testing against the latest Stripe version.
In this post, I'd like to suggest an alternative for that gem: replacing it with VCR for Stripe API requests, and JSON files for webhook events.
Using VCR to mock requests to the Stripe server
For tests that send API requests to Stripe, StripeMock starts a server to listen to those requests. They've implemented various request handlers, such as this PaymentIntent request handler.
I think it's a great work of them to try to mimic the input and output of Stripe server for all those requests. However, a mock server and a real server always have the risk of difference.
To address this, we can use VCR to let our tests make requests to the real Stripe server, recording the requests and responses for later test run.
Code using StripeMock:
require 'stripe_mock'
describe MyApp do
let(:stripe_helper) { StripeMock.create_test_helper }
before { StripeMock.start }
after { StripeMock.stop }
it "creates a stripe customer" do
customer = Stripe::Customer.create({
email: 'johnny@appleseed.com',
source: stripe_helper.generate_card_token
})
expect(customer.email).to eq('johnny@appleseed.com')
end
end
Code using VCR:
describe MyApp do
it "creates a stripe customer", :vcr do
customer = Stripe::Customer.create({
email: 'johnny@appleseed.com',
source: 'tok_visa'
})
expect(customer.email).to eq('johnny@appleseed.com')
end
end
I use the test-mode API key in the test
environment to make API requests on local. These requests are recorded in VCR cassettes, committed to git, and used to run tests in the CI environment. To avoid creating unnecessary data in your test mode, consider creating a separate Stripe account for automated test requests.
Using event JSON from the Stripe Dashboard to mock webhook requests
StripeMock can also mock webhook events
it "mocks a stripe webhook" do
event = StripeMock.mock_webhook_event('customer.created')
customer_object = event.data.object
expect(customer_object.id).to_not be_nil
expect(customer_object.default_card).to_not be_nil
# etc.
end
Under the hood, it creates a Stripe::Event object from JSON files, merging them with customized attributes.
The code is straightforward enough to maintain ourselves. Move it to a helper method in your codebase, and use your own JSON files.
# spec/support/stripe.rb
module StripeHelper
WEBHOOK_FIXTURE_PATH = Rails.root.join('spec/fixtures/stripe_webhooks').to_s
def mock_webhook_event(type, params = {})
fixture_path = File.join(WEBHOOK_FIXTURE_PATH, "#{type}.json")
unless File.exist?(fixture_path)
raise ArgumentError.new("No fixture file found for webhook event `#{type}`.")
end
event_data = JSON.parse(File.read(fixture_path))
event_data = ::Stripe::Util.symbolize_names(event_data)
params = ::Stripe::Util.symbolize_names(params)
event_data[:account] = params.delete(:account) if params.key?(:account)
event_data[:data][:object] = rmerge(event_data[:data][:object], params)
event_data[:created] = params[:created] || Time.current.to_i
event_data[:id] = "evt_#{SecureRandom.alphanumeric}"
::Stripe::Event.construct_from(event_data)
end
end
You can easily acquired the events' JSON data from the Developer dashboard on Stripe.
Conclusion
While stripe-ruby-mock is a great gem for quickly testing your Stripe integration, alternatives are always available. Personally, I prefer keeping my entire Gemfile up-to-date, so bidding farewell to the gem was my best choice. I hope this post gives you some ideas if you're aiming for the same goal.
Discussion