Using specific VCR cassette based on request
Asked Answered
T

3

6

Situation: testing a rails application using Rspec, FactoryGirl and VCR.

Every time a User is created, an associated Stripe customer is created through Stripe's API. While testing, it doesn't really makes sense to add a VCR.use_cassette or describe "...", vcr: {cassette_name: 'stripe-customer'} do ... to every spec where User creation is involved. My actual solution is the following:

RSpec.configure do |config|
  config.around do |example|
    VCR.use_cassette('stripe-customer') do |cassette|
      example.run
    end
  end
end

But this isn't sustainable because the same cassette will be used for every http request, which of course is very bad.

Question: How can I use specific fixtures (cassettes) based on individual request, without specifying the cassette for every spec?

I have something like this in mind, pseudo-code:

stub_request(:post, "api.stripe.com/customers").with(File.read("cassettes/stripe-customer"))

Relevant pieces of code (as a gist):

# user_observer.rb

class UserObserver < ActiveRecord::Observer

  def after_create(user)
    user.create_profile!

    begin
      customer =  Stripe::Customer.create(
        email: user.email,
        plan: 'default'
        )

      user.stripe_customer_id = customer.id
      user.save!
    rescue Stripe::InvalidRequestError => e
      raise e
    end

  end
end


# vcr.rb

require 'vcr'

VCR.configure do |config|
  config.default_cassette_options = { record: :once, re_record_interval: 1.day }
  config.cassette_library_dir = 'spec/fixtures/cassettes'
  config.hook_into :webmock
  config.configure_rspec_metadata!
end


# user_spec.rb

describe :InstanceMethods do
  let(:user) { FactoryGirl.create(:user) }

  describe "#flexible_name" do
    it "returns the name when name is specified" do
      user.profile.first_name = "Foo"
      user.profile.last_name = "Bar"

      user.flexible_name.should eq("Foo Bar")
    end
  end
end

Edit

I ended doing something like this:

VCR.configure do |vcr|
  vcr.around_http_request do |request|

    if request.uri =~ /api.stripe.com/
      uri = URI(request.uri)
      name = "#{[uri.host, uri.path, request.method].join('/')}"
      VCR.use_cassette(name, &request)

    elsif request.uri =~ /twitter.com/
      VCR.use_cassette('twitter', &request)
    else
    end

  end
end
Tabasco answered 14/5, 2013 at 1:57 Comment(0)
B
14

VCR 2.x includes a feature specifically to support use cases like these:

https://relishapp.com/vcr/vcr/v/2-4-0/docs/hooks/before-http-request-hook! https://relishapp.com/vcr/vcr/v/2-4-0/docs/hooks/after-http-request-hook! https://relishapp.com/vcr/vcr/v/2-4-0/docs/hooks/around-http-request-hook!

VCR.configure do |vcr|
  vcr.around_http_request(lambda { |req| req.uri =~ /api.stripe.com/ }) do |request|
    VCR.use_cassette(request.uri, &request)
  end
end
Berte answered 14/5, 2013 at 5:21 Comment(2)
This is great for granularly selecting a cassette, but it only works for a single request. Imagine I need to use another cassette based on both the request method and uri. I may be seeing this from the wrong point of view but imho it would be nice to use VCR's request matchers inside around_http_request to insert a specific cassette based on the request match.Tabasco
If you need to use a different cassette based on the method and uri, then you can put any conditional logic you want in the hook to set the cassette name and options based on the specific request. I updated the example to name the cassette based on the request URI. Note that the lambda arg is optional; it's just a simple way to make the hook apply to only some requests, so you don't have to put extra guard conditionals in the hook itself.Berte
A
2

IMO, libraries like this should provided you with a mock class, but w/e.


You can do your pseudocode example already with Webmock, which is the default internet mocking library that VCR uses.

body = YAML.load(File.read 'cassettes/stripe-customer.yml')['http_interactions'][0]['response']['body']['string']
stub_request(:post, "api.stripe.com/customers").to_return(:body => body)

You could put that in a before block that only runs on a certain tag, then tag the requests that make API calls.


In their tests, they override the methods that delegate to RestClient (link). You could do this as well, take a look at their test suite to see how they use it, in particular their use of test_response. I think this is a terribly hacky way of doing things, and would feel really uncomfortable with it (note that I'm in the minority with this discomfort) but it should work for now (it has the potential to break without you knowing until runtime). If I were to do this, I'd want to build out real objects for the two mocks (the one mocking rest-client, and the other mocking the rest-client response).

Aziza answered 14/5, 2013 at 2:49 Comment(0)
G
-1

The whole point (mostly anyway) of VCR to just to replay the response of a previous request. If you are in there picking and choosing what response goes back to what request, you are quote/unquote doing-it-wrong.

Like Joshua already said, you should use Webmock for something like this. That's what VCR is uing behind the scenes anyway.

Galantine answered 14/5, 2013 at 6:41 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.