How to mock and stub active record before_create callback with factory_girl
Asked Answered
M

6

17

I have an ActiveRecord Model, PricePackage. That has a before_create call back. This call back uses a 3rd party API to make a remote connection. I am using factory girl and would like to stub out this api so that when new factories are built during testing the remote calls are not made.

I am using Rspec for mocks and stubs. The problem i'm having is that the Rspec methods are not available within my factories.rb

model:

class PricePackage < ActiveRecord::Base
    has_many :users
    before_create :register_with_3rdparty

    attr_accessible :price, :price_in_dollars, :price_in_cents, :title


    def register_with_3rdparty
      return true if self.price.nil?

        begin
          3rdPartyClass::Plan.create(
            :amount => self.price_in_cents,
            :interval => 'month',
            :name => "#{::Rails.env} Item #{self.title}",
            :currency => 'usd',
            :id => self.title)
        rescue Exception => ex
          puts "stripe exception #{self.title} #{ex}, using existing price"
          plan = 3rdPartyClass::Plan.retrieve(self.title)
          self.price_in_cents = plan.amount
          return true
        end
    end

factory:

#PricePackage
Factory.define :price_package do |f|
  f.title "test_package"
  f.price_in_cents "500"
  f.max_domains "20"
  f.max_users "4"
  f.max_apps "10"
  f.after_build do |pp|
    #
    #heres where would like to mock out the 3rd party response
    #
    3rd_party = mock()
    3rd_party.stub!(:amount).price_in_cents
    3rdPartyClass::Plan.stub!(:create).and_return(3rd_party)
  end
end

I'm not sure how to get the rspec mock and stub helpers loaded into my factories.rb and this might not be the best way to handle this.

Mayes answered 25/9, 2011 at 5:12 Comment(2)
As an aside, when you assign a bounty to a question that bounty will be taken from your reputation regardless of whether you assign it so it's a nice thing to do to follow through on it and allocate it to one of the answers that people give. Without doing that it simply evaporatesLingwood
Does pp.stub(:register_with_3rdparty){ true } in after_build raise any errors?Hetrick
T
19

As the author of the VCR gem, you'd probably expect me to recommend it for cases like these. I do indeed recommend it for testing HTTP-dependent code, but I think there's an underlying problem with your design. Don't forget that TDD (test-driven development) is meant to be a design discipline, and when you find it painful to easily test something, that's telling you something about your design. Listen to your tests' pain!

In this case, I think your model has no business making the 3rd party API call. It's a pretty significant violation of the single responsibility principle. Models should be responsible for the validation and persistence of some data, but this is definitely beyond that.

Instead, I would recommend you move the 3rd party API call into an observer. Pat Maddox has a great blog post discussing how observers can (and should) be used to loosely couple things without violating the SRP (single responsibility principle), and how that makes testing, much, much easier, and also improves your design.

Once you've moved that into an observer, it's easy enough to disable the observer in your unit tests (except for the specific tests for that observer), but keep it enabled in production and in your integration tests. You can use Pat's no-peeping-toms plugin to help with this, or, if you're on rails 3.1, you should check out the new functionality built in to ActiveModel that allows you to easily enable/disable observers.

Torrez answered 22/10, 2011 at 18:44 Comment(6)
What if the API call is the persistence (kind of like what ActiveResource does)? It seems inappropriate for that not to be in the model.Bailment
Sure, if the data is being persisted over an HTTP API, then yes, that would be the main responsibility of the model, and it should absolutely be in the model (or in superclass or module mixed into the model). Notice that I said "3rd party API call". I would never consider your persistence to be a 3rd part API.Torrez
So to my eyes this seems to go against the foundations of Factories -- that resources in your tests should be created organically and naturally, just as they would in an actual app, and that when you change your app code, your factory stays up to date. If I take an after_create filter from my model and move it to an observer which is then disabled, that is fundamentally a new state which isin't in the actual app. Doesn't this then make nonsense of tests? (Especially higher level controller or integration tests.)Reynaldoreynard
@PeterEhrlich - I used to think exactly like you do, and I wound up with rails apps that had brittle test suites that took 45 minutes+ to run. The problem is that you add a ton of time to your test suite to have it take all the same code paths to get into a particular state (rather than quickly constructing it directly in that state) and if you change something in those code paths, it can break a ton of tests (since they all depend on that code path working). You should absolutely have some tests like this, but I limit it to a handful of end-to-end acceptance tests.Torrez
url to blog post is downVespasian
Bolog post now at patmaddox.wordpress.com/2007/11/22/…Tsana
T
2

Checkout the VCR gem (https://www.relishapp.com/myronmarston/vcr). It will record your test suite's HTTP interactions and play them back for you. Removing any requirement to actually make HTTP connections to 3rd party API's. I've found this to be a much simpler approach than mocking the interaction out manually. Here's an example using a Foursquare library.

VCR.config do |c|
  c.cassette_library_dir = 'test/cassettes'
  c.stub_with :faraday
end

describe Checkin do
  it 'must check you in to a location' do
    VCR.use_cassette('foursquare_checkin') do
      Skittles.checkin('abcd1234') # Doesn't actually make any HTTP calls.
                                   # Just plays back the foursquare_checkin VCR
                                   # cassette.
    end
  end
end
Truant answered 8/10, 2011 at 10:53 Comment(0)
L
1

Although I can see the appeal in terms of encapsulation, the 3rd party stubbing doesn't have to happen (and in some ways perhaps shouldn't happen) within your factory.

Instead of encapsulating it in the factory you can simply define it at the start of your RSpec tests. Doing this also ensures that the assumptions of your tests are clear and stated at the start (which can be very helpful when debugging)

Before any tests that use PricePlan, setup the desired response and then return it from the 3rd party .create method:

before(:all) do
  3rd_party = mock('ThirdParty')
  3rdPartyClass::Plan.stub(:create).and_return(true)
end  

This should allow you to call the method but will head off the remote call.

*It looks like your 3rd Party stub has some dependencies on the original object (:price_in_cents) however without knowing more about the exact dependency I can't guess what would be the appropriate stubbing (or if any is necessary)*

Lingwood answered 10/10, 2011 at 13:34 Comment(2)
doesn't seem to be working: TypeError: #<RR::DoubleDefinitions::DoubleDefinition:0xc552f74> is not a class/moduleLanctot
here is another problem with this approach... normally, i would wholeheartedly agree WRT keeping the tests clean. in this case, however, since the model is likely to be used - directly or indirectly - in many dozens of tests, you would have to do this in every single test... hence @Mayes looking for encapsulation (inside factory)Lanctot
F
0

FactoryGirl can stub out an object's attributes, maybe that can help you:

# Returns an object with all defined attributes stubbed out
stub = FactoryGirl.build_stubbed(:user)

You can find more info in FactoryGirl's rdocs

Fariss answered 9/10, 2011 at 14:28 Comment(0)
L
0

I had the same exact issue. Observer discussion aside (it might be the right approach), here is what worked for me (it's a start and can/should be improved upon):

add a file 3rdparty.rb to spec/support with these contents:

RSpec.configure do |config|
  config.before do
    stub(3rdPartyClass::Plan).create do
     [add stuff here]
    end
  end
end

And make sure that your spec_helper.rb has this:

  Dir[Rails.root.join("spec/support/**/*.rb")].each { |f| require f }
Lanctot answered 31/1, 2012 at 0:3 Comment(0)
F
-1

Well, first, you're right that 'mock and stub' are not the language of Factory Girl

Guessing at your model relationships, I think you'll want to build another object factory, set its properties, and then associate them.

#PricePackage
Factory.define :price_package do |f|
  f.title "test_package"
  f.price_in_cents "500"
  f.max_domains "20"
  f.max_users "4"
  f.max_apps "10"
  f.after_build do |pp|
  f.3rdClass { Factory(:3rd_party) }
end

Factory.define :3rd_party do |tp|
  tp.price_in_cents = 1000
end

Hopefully I didn't mangle the relationship illegibly.

Faze answered 26/9, 2011 at 22:22 Comment(1)
There is no data association between price_package and the 3rd party stuff. I added an example of my model. Which helps demonstrate that the 3rdparty api is called within rails before_create callback. So I want to stub and mock that part within register_with_3rdparty method. So that factory girl is not connecting directly each time a new price_package factory is created.Mayes

© 2022 - 2024 — McMap. All rights reserved.