如何使用factory_girl模拟和存根活动记录before_create回调

发布于 2024-12-06 11:57:10 字数 1512 浏览 2 评论 0原文

我有一个 ActiveRecord 模型 PricePackage。那有一个 before_create 回调。此回调使用第 3 方 API 来建立远程连接。我正在使用 Factory Girl,并且想删除这个 api,这样当在测试期间构建新工厂时,就不会进行远程调用。

我使用 Rspec 进行模拟和存根。 中不可用

我遇到的问题是 Rspec 方法在我的 Factory.rb模型

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

:工厂:

#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

我不确定如何将 rspec 模拟和存根帮助程序加载到我的 Factory.rb 中,这可能不是最好的处理这个问题的方法。

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.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(6

烟燃烟灭 2024-12-13 11:57:10

作为 VCR gem 的作者,您可能希望我为此类案例推荐它。我确实推荐它来测试依赖于 HTTP 的代码,但我认为您的设计存在根本问题。不要忘记 TDD(测试驱动开发)是一门设计学科,当您发现轻松测试某些东西很痛苦时,这会告诉您一些有关您的设计的信息。听听你的测试的痛苦!

在这种情况下,我认为您的模型没有必要进行第 3 方 API 调用。这是对单一责任原则的严重违反。模型应该负责某些数据的验证和持久化,但这绝对超出了这个范围。

相反,我建议您将第 3 方 API 调用移至观察者中。 Pat Maddox 有一篇很棒的博文讨论如何使用观察者(并且应该)在不违反 SRP(单一职责原则)的情况下松散耦合事物,以及如何使测试变得非常非常容易,并且还可以改进你的设计。

一旦将其移至观察者中,就可以很容易地在单元测试中禁用观察者(该观察者的特定测试除外),但在生产和集成测试中保持其启用状态。您可以使用 Pat 的 no-peeping-toms 插件来帮助解决此问题,或者,如果你使用的是 Rails 3.1,你应该查看 内置新功能 ActiveModel 允许您轻松启用/禁用观察者

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.

最近可好 2024-12-13 11:57:10

查看 VCR gem (https://www.relishapp.com/myronmarston/vcr)。它将记录您的测试套件的 HTTP 交互并为您回放。消除了与第 3 方 API 实际建立 HTTP 连接的任何要求。我发现这是比手动模拟交互更简单的方法。这是使用 Foursquare 库的示例。

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

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
<逆流佳人身旁 2024-12-13 11:57:10

尽管我可以看到封装方面的吸引力,但第 3 方存根不必在您的工厂内发生(并且在某些方面也许不应该发生)。

您可以简单地在 RSpec 测试开始时定义它,而不是将其封装在工厂中。这样做还可以确保测试的假设在开始时就明确并说明(这在调试时非常有帮助)

在使用 PricePlan 的任何测试之前,设置所需的响应,然后从第 3 方返回它 .create 方法:

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

这应该允许您调用该方法,但会阻止远程调用。

*看起来您的第 3 方存根对原始对象 (:price_in_cents) 有一些依赖项,但是如果不了解更多有关确切依赖项的信息,我无法猜测适当的存根是什么(或者是否有必要)*

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)*

白况 2024-12-13 11:57:10

FactoryGirl 可以存根对象的属性,也许这可以帮助您:

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

您可以在 FactoryGirl 的 rdocs

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

莫相离 2024-12-13 11:57:10

我有同样的问题。撇开观察者讨论不谈(这可能是正确方法),这对我有用(这是一个开始,可以/应该改进):

将文件 3rdparty.rb 添加到规范/支持中内容:

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

并确保您的spec_helper.rb具有以下内容:

  Dir[Rails.root.join("spec/support/**/*.rb")].each { |f| require f }

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 }
冷情妓 2024-12-13 11:57:10

好吧,首先,你是对的,“模拟和存根”不是工厂女孩的语言。

猜测你的模型关系,我认为你会想要构建另一个对象工厂,设置其属性,然后关联它们。

#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

希望我没有把这段关系搞得难以辨认。

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.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文