可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
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.
回答1:
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.
回答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
回答3:
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)*
回答4:
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
回答5:
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 }
回答6:
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.