Пропустить обратные вызовы на Factory Girl и Rspec


103

Я тестирую модель с обратным вызовом после создания, которую я хотел бы запускать только в некоторых случаях во время тестирования. Как я могу пропустить / запустить обратные вызовы с завода?

class User < ActiveRecord::Base
  after_create :run_something
  ...
end

Завод:

FactoryGirl.define do
  factory :user do
    first_name "Luiz"
    last_name "Branco"
    ...
    # skip callback

    factory :with_run_something do
      # run callback
  end
end

Ответы:


111

Я не уверен, что это лучшее решение, но я успешно добился этого, используя:

FactoryGirl.define do
  factory :user do
    first_name "Luiz"
    last_name "Branco"
    #...

    after(:build) { |user| user.class.skip_callback(:create, :after, :run_something) }

    factory :user_with_run_something do
      after(:create) { |user| user.send(:run_something) }
    end
  end
end

Запуск без обратного вызова:

FactoryGirl.create(:user)

Запуск с обратным вызовом:

FactoryGirl.create(:user_with_run_something)

3
Если вы хотите пропустить :on => :createвалидацию, используйтеafter(:build) { |user| user.class.skip_callback(:validate, :create, :after, :run_something) }
Джеймс Шевалье

8
не лучше ли было бы инвертировать логику пропуска обратного вызова? Я имею в виду, что по умолчанию, когда я создаю объект, срабатывают обратные вызовы, и я должен использовать другой параметр для исключительного случая. поэтому FactoryGirl.create (: user) должен создать пользователя, запускающего обратные вызовы, а FactoryGirl.create (: user_without_callbacks) должен создать пользователя без обратных вызовов. Я знаю, что это всего лишь модификация «дизайна», но я думаю, что это поможет избежать нарушения уже существующего кода и будет более последовательным.
Gnagno

3
Как отмечает решение @ Minimal, Class.skip_callbackвызов будет постоянным в других тестах, поэтому, если другие ваши тесты ожидают, что обратный вызов произойдет, они потерпят неудачу, если вы попытаетесь инвертировать логику пропуска обратного вызова.
mpdaugherty

В итоге я использовал ответ @ uberllama о заглушке с Mocha в after(:build)блоке. Это позволяет заводским настройкам по умолчанию запускать обратный вызов и не требует сброса обратного вызова после каждого использования.
mpdaugherty

Есть ли у вас какие-либо мысли о том, что это работает по-другому? stackoverflow.com/questions/35950470/…
Крис Хаф,

90

Если вы не хотите запускать обратный вызов, сделайте следующее:

User.skip_callback(:create, :after, :run_something)
Factory.create(:user)

Имейте в виду, что skip_callback будет постоянным в других спецификациях после его запуска, поэтому рассмотрите следующее:

before do
  User.skip_callback(:create, :after, :run_something)
end

after do
  User.set_callback(:create, :after, :run_something)
end

12
Мне этот ответ больше нравится, потому что в нем явно указано, что пропуск обратных вызовов зависает на уровне класса и, следовательно, будет продолжать пропускать обратные вызовы в последующих тестах.
siannopollo

Мне это тоже больше нравится. Я не хочу, чтобы мой завод постоянно вел себя по-другому. Я хочу пропустить его для определенного набора тестов.
theUtherSide 04

39

Ни одно из этих решений не является хорошим. Они искажают класс, удаляя функциональные возможности, которые должны быть удалены из экземпляра, а не из класса.

factory :user do
  before(:create){|user| user.define_singleton_method(:send_welcome_email){}}

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


1
Мне очень нравится этот ответ, и я задаюсь вопросом, должно ли что-то подобное с псевдонимом, чтобы намерение было сразу понятно, быть частью самой FactoryGirl.
Джузеппе

Мне также так нравится этот ответ, я бы проголосовал против всего остального, но, похоже, нам нужно передать блок определенному методу, если ваш обратный вызов является родственным around_*(например user.define_singleton_method(:around_callback_method){|&b| b.call }).
Quv

1
Не только лучшее решение, но и по какой-то причине другой метод у меня не сработал. Когда я реализовал его, он сказал, что метода обратного вызова не существует, но когда я его оставил, он попросил меня заглушить ненужные запросы. Хотя я привел меня к решению, кто-нибудь знает, почему это могло быть?
Babbz77

27

Я хотел бы улучшить ответ @luizbranco, чтобы сделать обратный вызов after_save более повторно используемым при создании других пользователей.

FactoryGirl.define do
  factory :user do
    first_name "Luiz"
    last_name "Branco"
    #...

    after(:build) { |user| 
      user.class.skip_callback(:create, 
                               :after, 
                               :run_something1,
                               :run_something2) 
    }

    trait :with_after_save_callback do
      after(:build) { |user| 
        user.class.set_callback(:create, 
                                :after, 
                                :run_something1,
                                :run_something2) 
      }
    end
  end
end

Запуск без обратного вызова after_save:

FactoryGirl.create(:user)

Запуск с обратным вызовом after_save:

FactoryGirl.create(:user, :with_after_save_callback)

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

---------- ОБНОВЛЕНИЕ ------------ Я перестал использовать skip_callback, потому что в наборе тестов были проблемы с несогласованностью.

Альтернативное решение 1 (использование заглушки и заглушки):

after(:build) { |user| 
  user.class.any_instance.stub(:run_something1)
  user.class.any_instance.stub(:run_something2)
}

trait :with_after_save_callback do
  after(:build) { |user| 
    user.class.any_instance.unstub(:run_something1)
    user.class.any_instance.unstub(:run_something2)
  }
end

Альтернативное решение 2 (мой предпочтительный подход):

after(:build) { |user| 
  class << user
    def run_something1; true; end
    def run_something2; true; end
  end
}

trait :with_after_save_callback do
  after(:build) { |user| 
    class << user
      def run_something1; super; end
      def run_something2; super; end
    end
  }
end

Есть ли у вас какие-либо мысли о том, что это работает по-другому? stackoverflow.com/questions/35950470/…
Крис Хаф

RuboCop жалуется на «Style / SingleLineMethods: Избегайте однострочных определений методов» для Альтернативного решения 2, поэтому мне нужно изменить форматирование, но в остальном это идеально!
coberlin

15

Rails 5 - skip_callbackвозникает ошибка аргумента при переходе с фабрики FactoryBot.

ArgumentError: After commit callback :whatever_callback has not been defined

В Rails 5 произошло изменение в том, как skip_callback обрабатывает нераспознанные обратные вызовы:

ActiveSupport :: Callbacks # skip_callback теперь вызывает ArgumentError, если нераспознанный обратный вызов удаляется.

Когда skip_callbackвызывается с завода, реальный обратный вызов в модели AR еще не определен.

Если вы все перепробовали и вытащили волосы, как я, вот ваше решение (взято из поиска проблем FactoryBot) ( ЗАПОМНИТЕ эту raise: falseчасть ):

after(:build) { YourSweetModel.skip_callback(:commit, :after, :whatever_callback, raise: false) }

Не стесняйтесь использовать его с любыми другими стратегиями, которые вам нравятся.


1
Отлично, именно это и случилось со мной. Обратите внимание: если вы удалили обратный вызов один раз и попробовали его снова, это произойдет, поэтому весьма вероятно, что он будет запускаться несколько раз для фабрики.
суббота,

6

Это решение работает для меня, и вам не нужно добавлять дополнительный блок в определение Factory:

user = FactoryGirl.build(:user)
user.send(:create_without_callbacks) # Skip callback

user = FactoryGirl.create(:user)     # Execute callbacks

5
FactoryGirl.define do
  factory :order, class: Spree::Order do

    trait :without_callbacks do
      after(:build) do |order|
        order.class.skip_callback :save, :before, :update_status!
      end

      after(:create) do |order|
        order.class.set_callback :save, :before, :update_status!
      end
    end
  end
end

Важное примечание: вы должны указать их оба. Если использовать только ранее и запустить несколько спецификаций, он попытается отключить обратный вызов несколько раз. В первый раз это будет успешно, но во второй обратный вызов больше не будет определяться. Так что это ошибка


Это вызвало некоторые запутанные сбои в наборе в недавнем проекте - у меня было что-то похожее на ответ @Sairam, но обратный вызов оставался неустановленным в классе между тестами. Упс.
kfrz

5

В Rspec 3 мне больше всего подошла простая заглушка

allow_any_instance_of(User).to receive_messages(:run_something => nil)

5
Вы должны были бы установить его для случаев с User; :run_somethingне является методом класса.
PAOopeland

4

Вызов skip_callback с моего завода оказался для меня проблематичным.

В моем случае у меня есть класс документа с некоторыми обратными вызовами, связанными с s3, до и после создания, которые я хочу запускать только тогда, когда необходимо тестирование полного стека. В противном случае я хочу пропустить эти обратные вызовы s3.

Когда я попробовал skip_callbacks на своей фабрике, он сохранил этот пропуск обратного вызова, даже когда я создал объект документа напрямую, без использования фабрики. Поэтому вместо этого я использовал заглушки мокко в вызове после сборки, и все работает отлично:

factory :document do
  upload_file_name "file.txt"
  upload_content_type "text/plain"
  upload_file_size 1.kilobyte
  after(:build) do |document|
    document.stubs(:name_of_before_create_method).returns(true)
    document.stubs(:name_of_after_create_method).returns(true)
  end
end

Из всех решений здесь, и за то, что логика в заводе, это только один , который работает с before_validationкрючком (пытается сделать skip_callbackс любым из FactoryGirl - х beforeили afterвариантов buildи createне работают)
Mike T

3

Это будет работать с текущим синтаксисом rspec (начиная с этого сообщения) и намного чище:

before do
   User.any_instance.stub :run_something
end

в Rspec 3 это устарело. У меня сработало использование обычной заглушки, см. мой ответ ниже.
samg

3

Ответ Джеймса Шевалье о том, как пропустить обратный вызов before_validation, мне не помог, поэтому, если вы отказываетесь от того же, что и я, вот рабочее решение:

в модели:

before_validation :run_something, on: :create

на заводе:

after(:build) { |obj| obj.class.skip_callback(:validation, :before, :run_something) }

2
Я считаю, что этого лучше избегать. Он пропускает обратные вызовы для каждого экземпляра класса (а не только для тех, которые созданы фабричной девушкой). Это приведет к некоторым проблемам выполнения спецификации (например, если отключение произойдет после сборки начальной фабрики), отладить которые может быть сложно. Если это желаемое поведение в спецификации / поддержке, это должно быть сделано явно: Model.skip_callback(...)
Кевин Сильвестр,

2

В моем случае у меня есть обратный вызов, загружающий что-то в мой кеш Redis. Но тогда у меня не было / не нужно было запускать экземпляр Redis для моей тестовой среды.

after_create :load_to_cache

def load_to_cache
  Redis.load_to_cache
end

Для моей ситуации, аналогичной описанной выше, я просто вставил свой load_to_cacheметод в свой spec_helper с помощью:

Redis.stub(:load_to_cache)

Кроме того, в определенной ситуации, когда я хочу проверить это, мне просто нужно отключить их в блоке before соответствующих тестовых случаев Rspec.

Я знаю, что у вас может происходить что-то более сложное, after_createили это может показаться не очень элегантным. Вы можете попытаться отменить обратный вызов, определенный в вашей модели, определив after_createловушку в своем Factory (см. Документацию factory_girl), где вы, вероятно, можете определить тот же обратный вызов и возврат false, согласно разделу «Отмена обратных вызовов» этой статьи . (Я не уверен в том, в каком порядке выполняются обратные вызовы, поэтому я не выбрал этот вариант).

Наконец, (извините, я не могу найти статью) Ruby позволяет вам использовать грязное мета-программирование, чтобы отцепить перехватчик обратного вызова (вам придется его сбросить). Думаю, это наименее предпочтительный вариант.

Что ж, есть еще одна вещь, на самом деле не решение, но посмотрите, сможете ли вы уйти с Factory.build в своих спецификациях вместо фактического создания объекта. (Было бы проще, если бы можно).


2

Что касается ответа, опубликованного выше, https://stackoverflow.com/a/35562805/2001785 , вам не нужно добавлять код на заводе. Мне было проще перегрузить методы в самих спецификациях. Например, вместо (в сочетании с заводским кодом в цитируемом посте)

let(:user) { FactoryGirl.create(:user) }

Мне нравится использовать (без указанного заводского кода)

let(:user) do
  FactoryGirl.build(:user).tap do |u|
      u.define_singleton_method(:send_welcome_email){}
      u.save!
    end
  end
end

Таким образом, вам не нужно смотреть как на фабрику, так и на тестовые файлы, чтобы понять поведение теста.


1

Я нашел следующее решение более чистым, поскольку обратный вызов запускается / устанавливается на уровне класса.

# create(:user) - will skip the callback.
# create(:user, skip_create_callback: false) - will set the callback
FactoryBot.define do
  factory :user do
    first_name "Luiz"
    last_name "Branco"

    transient do
      skip_create_callback true
    end

    after(:build) do |user, evaluator|
      if evaluator.skip_create_callback
        user.class.skip_callback(:create, :after, :run_something)
      else
        user.class.set_callback(:create, :after, :run_something)
      end
    end
  end
end

0

Вот фрагмент, который я создал, чтобы справиться с этим общим способом.
Он пропустит все настроенные обратные вызовы, включая обратные вызовы, связанные с рельсами, например before_save_collection_association, но он не пропустит некоторые, необходимые для нормальной работы ActiveRecord, например автоматически сгенерированные autosave_associated_records_for_обратные вызовы.

# In some factories/generic_traits.rb file or something like that
FactoryBot.define do
  trait :skip_all_callbacks do
    transient do
      force_callbacks { [] }
    end

    after(:build) do |instance, evaluator|
      klass = instance.class
      # I think with these callback types should be enough, but for a full
      # list, check `ActiveRecord::Callbacks::CALLBACKS`
      %i[commit create destroy save touch update].each do |type|
        callbacks = klass.send("_#{type}_callbacks")
        next if callbacks.empty?

        callbacks.each do |cb|
          # Autogenerated ActiveRecord after_create/after_update callbacks like
          # `autosave_associated_records_for_xxxx` won't be skipped, also
          # before_destroy callbacks with a number like 70351699301300 (maybe
          # an Object ID?, no idea)
          next if cb.filter.to_s =~ /(autosave_associated|\d+)/

          cb_name = "#{klass}.#{cb.kind}_#{type}(:#{cb.filter})"
          if evaluator.force_callbacks.include?(cb.filter)
            next Rails.logger.debug "Forcing #{cb_name} callback"
          end

          Rails.logger.debug "Skipping #{cb_name} callback"
          instance.define_singleton_method(cb.filter) {}
        end
      end
    end
  end
end

тогда позже:

create(:user, :skip_all_callbacks)

Излишне говорить, что YMMV, так что посмотрите в журналах тестов, что вы действительно пропускаете. Возможно, у вас есть драгоценный камень, добавляющий обратный вызов, который вам действительно нужен, и он заставит ваши тесты с треском провалиться, или из вашей жирной модели из 100 обратных вызовов вам просто нужна пара для конкретного теста. В таких случаях попробуйте переходный:force_callbacks

create(:user, :skip_all_callbacks, force_callbacks: [:some_important_callback])

БОНУС

Иногда вам нужно также пропустить проверки (все, чтобы сделать тесты быстрее), а затем попробуйте:

  trait :skip_validate do
    to_create { |instance| instance.save(validate: false) }
  end

-1
FactoryGirl.define do
 factory :user do
   first_name "Luiz"
   last_name "Branco"
   #...

after(:build) { |user| user.class.skip_callback(:create, :after, :run_something) }

trait :user_with_run_something do
  after(:create) { |user| user.class.set_callback(:create, :after, :run_something) }
  end
 end
end

Вы можете просто установить обратный вызов с трейтом для тех экземпляров, когда захотите его запустить.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.