Пропустить обратные вызовы на Factory Девушка и Rspec
Я тестирую модель с последующим обратным вызовом после создания, который я бы хотел запускать только в некоторых случаях во время тестирования. Как я могу пропускать/запускать обратные вызовы из factory?
class User < ActiveRecord::Base
after_create :run_something
...
end
Factory:
FactoryGirl.define do
factory :user do
first_name "Luiz"
last_name "Branco"
...
# skip callback
factory :with_run_something do
# run callback
end
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) }
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)
Ответ 2
Если вы не хотите запускать обратный вызов, выполните следующие действия:
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
Ответ 3
Ни одно из этих решений не является хорошим. Они деактивируют класс, удаляя функциональность, которая должна быть удалена из экземпляра, а не из класса.
factory :user do
before(:create){|user| user.define_singleton_method(:send_welcome_email){}}
Вместо подавления обратного вызова я подавляю функциональность обратного вызова. В некотором смысле, мне нравится этот подход лучше, потому что он более явный.
Ответ 4
Я хотел бы сделать улучшение ответа @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 (использование заглушки и unub):
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
Ответ 5
Это решение работает для меня, и вам не нужно добавлять дополнительный блок в определение Factory:
user = FactoryGirl.build(:user)
user.send(:create_without_callbacks) # Skip callback
user = FactoryGirl.create(:user) # Execute callbacks
Ответ 6
Простой заглушка работал лучше всего для меня в Rspec 3
allow(User).to receive_messages(:run_something => nil)
Ответ 7
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
Важное примечание. Вы должны указать оба из них.
Если использовать только и запускать несколько спецификаций, он будет пытаться отключить обратный вызов несколько раз. Это будет успешным в первый раз, но во втором, обратный вызов больше не будет определен. Таким образом, это приведет к ошибке
Ответ 8
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) }
Не стесняйтесь использовать его с любыми другими стратегиями, которые вы предпочитаете.
Ответ 9
Вызов skip_callback из моего factory оказался проблематичным для меня.
В моем случае у меня есть класс документа с некоторыми связанными с s3 обратными вызовами до и после создания, которые я хочу использовать только при проверке полного стека. В противном случае я хочу пропустить эти вызовы s3.
Когда я пробовал skip_callbacks в моем factory, он продолжал пропускать обратный вызов, даже когда я создал объект документа напрямую, без использования factory. Поэтому вместо этого я использовал mocha-заглушки в вызове после сборки, и все работает отлично:
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
Ответ 10
Это будет работать с текущим синтаксисом rspec (начиная с этого сообщения) и намного чище:
before do
User.any_instance.stub :run_something
end
Ответ 11
Джеймс Шевалье отвечает о том, как пропустить перед обратным вызовом before_validation, не помог мне, поэтому, если вы нарушите то же самое, что и я, здесь находится работающее решение:
в модели:
before_validation :run_something, on: :create
в factory:
after(:build) { |obj| obj.class.skip_callback(:validation, :before, :run_something) }
Ответ 12
В моем случае у меня есть обратный вызов, загружающий что-то в мой 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)
Кроме того, в определенной ситуации, когда я хочу протестировать это, мне просто нужно разбить их в предыдущем блоке соответствующих тестовых случаев Rspec.
Я знаю, что у вас может быть что-то более сложное в вашем after_create
или, возможно, не будет выглядеть очень элегантно. Вы можете попытаться отменить обратный вызов, определенный в вашей модели, определив крюк after_create
в Factory (см. Документы factory_girl), где вы, вероятно, можете определить один и тот же обратный вызов и вернуть false
, в соответствии с ' Отмена раздела обратных вызовов этой статьи . (Я не уверен в порядке выполнения обратного вызова, поэтому я не пошел на эту опцию).
Наконец, (извините, что я не могу найти статью) Ruby позволяет вам использовать грязное метапрограммирование, чтобы отцепить крючок обратного вызова (вам придется reset его). Думаю, это был бы наименее предпочтительный вариант.
Ну есть еще одна вещь, а не действительно решение, но посмотрите, можете ли вы уйти с Factory.build в своих спецификациях, а не фактически создавать объект. (Было бы проще, если бы вы могли).
Ответ 13
Что касается ответа выше, fooobar.com/questions/78341/..., вам не нужно добавлять код на завод. Мне было легче перегружать методы в самих спецификациях. Например, вместо (в сочетании с заводским кодом в цитируемом сообщении)
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
Таким образом, вам не нужно смотреть как на заводские, так и на тестовые файлы, чтобы понять поведение теста.
Ответ 14
Я нашел следующее решение более чистым, поскольку обратный вызов запускается/устанавливается на уровне класса.
# 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
Ответ 15
Вот фрагмент, который я создал, чтобы обработать это в общем виде.
Он будет пропускать все настроенные обратные вызовы, включая связанные с рельсами обратные вызовы, такие как
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"
klass.skip_callback(type, cb.kind, 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
Ответ 16
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
Вы можете просто установить обратный вызов с признаком для тех экземпляров, когда вы хотите его запустить.