DEV Community

Rob Race
Rob Race

Posted on

Testing Stripe with Rails and RSpec

While Stripe may not be available everywhere or may not be everyone’s most cost-efficient card processor, I have found time and time again that their API and Dashboard are well thought out and “just work”.

Testing Stripe will be a bit more involved than most another testing you will do within your application. Not only are you responsible for testing against the behavior of your application, but also keeping track of remote objects and responses on a third party API(Stripe). I am going to spare some of the details on getting Stripe and running in your application as it seems that it is covered many places and focus on some high-level techniques to test Stripe in your Rails application, using RSpec.


If this is the sort of content you like, then you will be able to see a full implementation(including a comprehensive test suite) from my book Build A SaaS App in Rails 5. The book guides you from humble beginnings through deploying an app to production. The book is nearly complete, and you can grab a free chapter right now!


stripe-ruby-mock

stripe-ruby-mock is a gem that allows you to test against Stripe without actually hitting the external Stripe API servers(unless you want to, and can explicitly do so). This will enable you to not worry about external connections, cleaning up test data and complete control over any mocked data.

Installation is pretty standard for a Rails app:

Add gem 'stripe-ruby-mock', '~> 2.5.0', :require => 'stripe_mock' and then follow that up with a bundle install . Then, to get it started, you can add the following to your spec_helper.rb :

config.before(:each) do
  @stripe_test_helper = StripeMock.create_test_helper
  StripeMock.start
end

config.after(:each) do
  StripeMock.stop
end
Enter fullscreen mode Exit fullscreen mode

Which will start the mock server and make a handful of helper methods(such as creating a plan or user in Stripe)

Once installed, you can follow along with this hypothetical Controller spec:

RSpec.describe PlansController, type: :controller do
  login_admin

describe 'GET #index' do
    it 'returns http success' do
      get :index
      expect(response).to have_http_status(:success)
      expect(assigns(:plans).count).to eq(3)
    end
  end

describe 'GET #show' do
    let(:plan) { @stripe_test_helper.create_plan(id: 'free', amount: 0) }
    before(:each) do
      PaymentServices::Stripe::Subscription::CreationService.(
        user: @admin,
        account: @admin.account,
        plan: plan.id
      )
      @admin.reload
    end

it 'returns http success starter token' do
      @stripe_test_helper.create_plan(id: 'starter', amount: 10)
      cus =
        Stripe::Customer
        .retrieve(@admin.account.subscription.stripe_customer_id)
      card = cus.sources.create(source: @stripe_test_helper.generate_card_token)
      @admin.account.subscription.update(stripe_token: card.id)
      get :show, params: {id: 'starter'}
      expect(response).to have_http_status(:redirect)
      expect(flash[:notice]).to eq 'Plan was updated successfully.'
    end
...
Enter fullscreen mode Exit fullscreen mode

In this controller spec(which is partial for the brevity of the example), there is a spec to make sure the index action displays the right number of plans based on the local or remote plan retrieval methods you use.

Personally, I like to use a local yml file that will sync plans on demand.

Next, the good stuff. The let definitions allow us to set up a plan in the mocked Stripe server, as well as creating a subscription(here based on a Service Object that handles Stripe Customer and Stripe Subscription creation). Inside the specific show spec, another plan is created(that requires a source/card) and a new source/card is added through the Stripe Ruby gem’s interface. In this case, Stripe want’s you to grab the Customer object, then make an update to sources through that customer. Once done, it is saved to the local subscription record. Lastly, the show action will do its thing now with a Stripe Customer, Subscription and Card already connected to the local user in the spec.

This is just a small sample of how to write controller actions that interface with Stripe. To move on, let’s see a feature spec that is hitting the sign up workflow, which now sports some Plans and Stripe API interactions.

require 'rails_helper'
include ActiveJob::TestHelper
ActiveJob::Base.queue_adapter = :test

RSpec.feature "SignUpProcesses", type: :feature do
  it "should require the user to sign up and successfully sign up" do

    visit root_path

    click_on 'Sign up'

    find(:xpath, "//a[='/account/plans/free']").click

    within "#new_user" do
      fill_in "user_name", with: 'Test'
      fill_in "user_email", with: 'test@test.com'
      fill_in "user_password", with: 'password123'
      fill_in "user_password_confirmation", with: 'password123'
    end

    click_button "Sign Up"

    expect(current_path).to eql(new_accounts_path)

    within "#new_account" do
      fill_in "account_name", with: "Test Co"
    end

    expect do
      click_button "Save"
      expect(ActionMailer::Base.deliveries.last.to).to eq ['']
      expect(current_path).to eql(root_path)
    end
  end
...
Enter fullscreen mode Exit fullscreen mode

While this doesn’t make any direct use of StripeMock’s helpers or server, it will actually save all Customer and Subscription changes to the local StripeMock instance(which, by the way get reset between every test).

Anyway, clicking Sign Up will take you to a plans#index page. On that page, you will be able to click on a link that will select the plan to begin a trial on. Once done, that plan will be passed through the continuation of the signup process, finally, being used within a Service Object, which delegates out the Stripe creations to the same CreationService class from before.

If there were any issues with the Stripe Subscriptions, the workflow would not end up on the root_path and fail tests. You can even take this a step further bu having the feature visit the Billing section of your application and make sure the plan that was selected is the plan displayed as current.

Webhooks!

What Stripe discussion isn’t complete without mentioning Webhooks. On the off chance you have never heard of Webhooks, it means that when a third party service makes a change that is not a direct result of the API call that it came from, it will send an HTTP request to the application to handle such an event. To build a more concrete example suppose there is a payment coming up in a few days. Stripe will create an event invoice.upcoming and broadcast a HTTP request to an endpoint the Rails application specified for receiving such events. From there, it is up to your application to handle the event.

Before jumping into the spec(s) around Stripe Webhooks. Let me go ahead and give a shout out to the StripeEvent gem. As a good software maintainer, I like to make sure a gem install is well worth it. Stripe_event is a tiny gem that sets up the WebhooksController, and one file set of tiny classes to add a little bit of metaprogramming. It uses ActiveSupport::Notifications under the hood to pass the events to you handling code(which you can choose between blocks or send a .call to a class of your choice).

With that being said, testing a webhook is another quick piece of setup through StripeMock.

require 'rails_helper'
include ActiveJob::TestHelper

RSpec.describe Payments::InvoicePaymentSucceeded, type: :mailer do
  let(:plan) { @stripe_test_helper.create_plan(id: 'free', amount: 0) }

  before(:each) do
    @admin = FactoryGirl.create(:user, email: 'awesomesauce@booyah.io')
    PaymentServices::Stripe::Subscription::CreationService.(
      user: @admin,
      account: @admin.account,
      plan: plan.id
    )
    @event = StripeMock.mock_webhook_event(
      'invoice.payment_succeeded',
      customer: @admin.account.subscription.stripe_customer_id,
      subscription: @admin.account.subscription.stripe_subscription_id
    )
  end

  it 'job is created' do
    ActiveJob::Base.queue_adapter = :test
    expect do
      Payments::InvoicePaymentSucceeded.email(@event.id).deliver_later
    end.to have_enqueued_job.on_queue('mailers')
  end

  it 'email is sent' do
    expect do
      perform_enqueued_jobs do
        Payments::InvoicePaymentSucceeded.email(@event.id).deliver_later
      end
    end.to change { ActionMailer::Base.deliveries.size }.by(1)
  end
...
Enter fullscreen mode Exit fullscreen mode

The setup looks pretty similar. A plan is added, through a StripeMock helper. Then, before each spec, an @admin user is created by FactoryGirl/FactoryBot. Once created, a Customer and Subscription are setup. Finally, a mock webhook event is created. This is the important piece here, as StripeEvent or your own Webhook handling code will be retrieving the Event from Stripe. Thus, our tests will need a mocked version.

By default, StripeMock actually keeps a copy of most webhook events in JSON fixtures within the gem’s structure. However, for each type of event, you can override specific attributes. Here, to make sure the event pulls up the right customer an subscription, they are overridden in the second argument of creating the mock event.

Once that is out of the way, it is pretty much business as normal. In the case of the beginning of the spec here, upon passing in an event to the mailer, it is making sure the mailer “did its thing” and queued up mail to the right queue.

Another cool piece of StripeMock is the ability to have the specific request throw an error and thus allowing you to test your error handling logic. The syntax is pretty simple, but will require you to look up the symbol definition of a particular request type/endpoint:

StripeMock.prepare_error(
  Stripe::InvalidRequestError.new('s', {}),
  :get_charges
)
Enter fullscreen mode Exit fullscreen mode

Here, we’re specifying the Stripe::InvalidRequestError to be raised the next time Stripe::Charge.list is invoked.

What do you use?

That is a high level over a few different places to test Stripe. As well as some tools that could help your Stripe implementation(and tests).

What do you use or implement when utilizing Stripe(and testing it as you should)?

Top comments (0)