Как обманывать и блокировать активную запись before_create callback с помощью factory_girl

У меня есть модель ActiveRecord, PricePackage. У этого есть вызов before_create. Этот обратный вызов использует сторонний API для удаленного подключения. Я использую factory девушку и хотел бы закрыть эту апи, чтобы при создании новых фабрик во время тестирования удаленные вызовы не выполнялись.

Я использую Rspec для mocks и stubs. Проблема, с которой я сталкиваюсь, заключается в том, что методы Rspec недоступны на моих фабриках. 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

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

Я не уверен, как получить rspec mock и помощников-заглушек, загруженных в мои фабрики .rb, и это может быть не лучший способ справиться с этим.

Ответы

Ответ 1

Как автор драгоценного камня VCR, вы, вероятно, ожидаете, что я рекомендую его для подобных случаев. Я действительно рекомендую его для тестирования HTTP-зависимого кода, но я думаю, что есть основная проблема с вашим дизайном. Не забывайте, что TDD (разработка, основанная на тестах) призвана стать дисциплиной дизайна, и когда вам будет легко испытать что-то, что говорит вам что-то о вашем дизайне. Слушайте боль в тестах!

В этом случае, я думаю, ваша модель не имеет бизнеса, делающего вызов сторонних API. Это довольно значительное нарушение принципа единой ответственности. Модели должны отвечать за проверку и сохранение некоторых данных, но это определенно выше этого.

Вместо этого я бы рекомендовал вам переместить вызов стороннего API в наблюдателя. У Пэта Мэддокса есть отличный пост в блоге, в котором обсуждается, как наблюдатели могут (и должны) использоваться, чтобы свободно сочетать вещи, не нарушая SRP (принцип единой ответственности) и как это делает тестирование, намного, намного проще, а также улучшает ваш дизайн.

Как только вы переместили это в наблюдателя, достаточно легко отключить наблюдателя в своих модульных тестах (за исключением конкретных тестов для этого наблюдателя), но сохранить его в процессе производства и в тестах интеграции. Вы можете использовать плагин Pat no-peeping-toms, чтобы помочь в этом, или, если вы находитесь на рейках 3.1, вы должны проверить новая функциональность, встроенная в ActiveModel, которая позволяет легко включать/отключать наблюдателей.

Ответ 2

Оформить заказ на видеомагнитофон (https://www.relishapp.com/myronmarston/vcr). Он будет записывать ваши тестовые пакеты HTTP-взаимодействия и воспроизводить их для вас. Удаление любых требований для фактического подключения HTTP к сторонним API. Я считаю, что это гораздо более простой подход, чем издевательствование взаимодействия вручную. Вот пример использования библиотеки 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

Ответ 3

Хотя я вижу апелляцию с точки зрения инкапсуляции, третья сторона не должна произойти (и в некотором роде, возможно, не должна произойти) в вашем factory.

Вместо того, чтобы инкапсулировать его в factory, вы можете просто определить его в начале ваших тестов RSpec. Это также гарантирует, что допущения ваших тестов ясны и указаны в начале (что может быть очень полезно при отладке)

Перед любыми тестами, которые используют PricePlan, настройте желаемый ответ, а затем верните его из стороннего метода .create:

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

Это должно позволить вам вызвать метод, но не удастся удалить удаленный вызов.

* Похоже, что ваш сторонний заглушка имеет некоторые зависимости от исходного объекта (: price_in_cents), однако, не зная больше о точной зависимости, я не могу догадаться, что было бы подходящим stubbing (или если это необходимо) *

Ответ 4

FactoryGirl может заглушить атрибуты объекта, возможно, это может помочь вам:

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

Дополнительную информацию можно найти в FactoryGirl rdocs

Ответ 5

У меня была такая же точная проблема. Обсуждение наблюдателя в стороне (это может быть правильный подход), вот что сработало для меня (это начало и может/должно быть улучшено):

добавить файл 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 }

Ответ 6

Хорошо, во-первых, вы правы, что "mock and stub" не являются языком Factory Girl

Угадав ваши отношения с моделью, я думаю, вы захотите создать еще один объект 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|
  f.3rdClass { Factory(:3rd_party) }
end

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

Надеюсь, я не исказил отношения неразборчиво.