Пропустіть зворотні виклики на 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) }
Джеймс Шевальє

7
хіба не було б краще інвертувати пропускну логіку зворотного виклику? Я маю на увазі, за замовчуванням має бути те, що коли я створюю об'єкт, спрацьовують зворотні виклики, і я повинен використовувати інший параметр для виняткового випадку. тому FactoryGirl.create (: користувач) повинен створити користувача, що викликає зворотні виклики, а FactoryGirl.create (: user_without_callbacks) повинен створити користувача без зворотних викликів. Я знаю, що це лише модифікація "дизайну", але я думаю, що це може уникнути порушення попереднього коду та бути більш послідовним.
Gnagno

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

Я в кінцевому підсумку скористався відповіддю @ uberllama про заїдання Mocha у after(:build)блоці. Це дозволяє заводському замовчуванню виконувати зворотний виклик і не потребує скидання зворотного дзвінка після кожного використання.
mpdaugherty

Чи маєте ви думки про це, працюючи іншим способом? stackoverflow.com/questions/35950470/…
Кріс Хаф

89

Коли ви не хочете запускати зворотний дзвінок, виконайте наступне:

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

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)

Запуск із зворотним зв'язком після_саве:

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 скаржиться на "Стиль / SingleLineMethods: уникайте визначення однорядних методів" для альтернативного рішення 2, тому мені потрібно змінити форматування, але в іншому випадку це ідеально!
коберлін

14

Rails 5 - skip_callbackпідвищення помилки аргументу при переході з заводу FactoryBot.

ArgumentError: After commit callback :whatever_callback has not been defined

У Rails 5 відбулася зміна з тим, як skip_callback обробляє невпізнані зворотні виклики:

ActiveSupport :: Зворотні виклики # skip_callback тепер викликає ArgumentError, якщо невпізнаний зворотний виклик буде видалено

Коли skip_callbackвикликається з фабрики, реальний зворотний виклик у моделі AR ще не визначений.

Якщо ви все випробували і витягнули волосся, як я, ось ваше рішення (отримано це від пошуку проблем FactoryBot) ( ПРИМІТКА raise: falseчастина ):

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

Не соромтеся використовувати його з будь-якими іншими стратегіями, які ви віддаєте перевагу.


1
Чудово, саме так сталося зі мною. Зауважте, що якщо ви видалили зворотний дзвінок один раз і спробуйте його ще раз, це станеться, тому цілком ймовірно, що це буде запускатися кілька разів на заводі.
slhck

6

Це рішення працює для мене, і вам не потрібно додавати додатковий блок до свого заводського визначення:

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

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

5

Простий заглушок найкраще працював для мене в Rspec 3

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

4
Ви повинні були б встановити його для випадків з User; :run_somethingне метод класу.
PJSCopeland

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

Важлива примітка, ви повинні вказати їх обох. Якщо використовувати лише раніше та запускати декілька специфікацій, вона намагатиметься відключити зворотній зв'язок кілька разів. Уперше це вдасться, але вдруге зворотний виклик більше не визначатиметься. Так що помилка вийде


Це спричинило деякі заплутані збої в наборі недавнього проекту - у мене було щось подібне до відповіді @ Сайрама, але зворотний виклик залишався невдалим у класі між тестами. Уопс.
kfrz

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не працювало)
Майк T

3

Це буде працювати з поточним синтаксисом rspec (станом на цю посаду) і набагато чистіше:

before do
   User.any_instance.stub :run_something
end

це застаріло в Rspec 3. Використовуючи звичайний заглушок, який працював на мене, дивіться мою відповідь нижче.
samg

3

Відповідь Джеймса Шевальє про те, як пропустити зворотний виклик до_ валідації, не допомогла мені, тому якщо ви задубите те саме, що я тут, це робоче рішення:

у моделі:

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)

Крім того, у певній ситуації, коли я хочу перевірити це, мені просто доведеться відкрутити їх у попередньому блоці відповідних тестів Rspec.

Я знаю, що у вас може статися щось складніше, after_createабо ви не можете вважати це дуже елегантним. Ви можете спробувати скасувати зворотний виклик, визначений у вашій моделі, визначивши after_createгачок у вашій Фабриці (див. Документи «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.