Пропустить обратные вызовы на 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

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