17

我有一个 ActiveRecord 模型 PricePackage。那有一个 before_create 回调。此回调使用第 3 方 API 进行远程连接。我正在使用工厂女孩,并希望删除此 api,以便在测试期间构建新工厂时不会进行远程调用。

我将 Rspec 用于模拟和存根。我遇到的问题是我的 factory.rb 中没有 Rspec 方法

模型:

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 中,这可能不是处理此问题的最佳方法。

4

6 回答 6

20

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

在这种情况下,我认为您的模型没有进行 3rd 方 API 调用的业务。这是对单一责任原则的严重违反。模型应该负责某些数据的验证和持久性,但这绝对不止于此。

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

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

于 2011-10-22T18:44:57.023 回答
2

查看 VCR gem (https://www.relishapp.com/myronmarston/vcr)。它将记录您的测试套件的 HTTP 交互并为您回放它们。删除任何与 3rd 方 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
于 2011-10-08T10:53:45.623 回答
1

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

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

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

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

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

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

于 2011-10-10T13:34:24.607 回答
0

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

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

您可以在FactoryGirl 的 rdocs中找到更多信息

于 2011-10-09T14:28:15.690 回答
0

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

使用以下内容将文件 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 }
于 2012-01-31T00:03:34.623 回答
-1

嗯,首先,你说得对,“mock and stub”不是工厂女孩的语言

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

#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

希望我没有难以理解地破坏这种关系。

于 2011-09-26T22:22:30.747 回答