Pomiń oddzwaniania do Factory Girl i Rspec


103

Testuję model z wywołaniem zwrotnym po utworzeniu, które chciałbym uruchamiać tylko czasami podczas testowania. Jak mogę pominąć / uruchomić wywołania zwrotne z fabryki?

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

Fabryka:

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

    factory :with_run_something do
      # run callback
  end
end

Odpowiedzi:


111

Nie jestem pewien, czy jest to najlepsze rozwiązanie, ale udało mi się to osiągnąć za pomocą:

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

Działa bez oddzwonienia:

FactoryGirl.create(:user)

Bieganie z oddzwonieniem:

FactoryGirl.create(:user_with_run_something)

3
Jeśli chcesz pominąć :on => :createwalidację, użyjafter(:build) { |user| user.class.skip_callback(:validate, :create, :after, :run_something) }
James Chevalier

8
czy nie byłoby lepiej odwrócić logikę pomijania wywołań zwrotnych? Chodzi mi o to, że domyślnym ustawieniem powinno być to, że kiedy tworzę obiekt, wywoływane są wywołania zwrotne i powinienem użyć innego parametru dla wyjątkowego przypadku. więc FactoryGirl.create (: user) powinno utworzyć użytkownika wyzwalającego wywołania zwrotne, a FactoryGirl.create (: user_without_callbacks) powinno utworzyć użytkownika bez wywołań zwrotnych. Wiem, że to tylko modyfikacja „projektowa”, ale myślę, że pozwala to uniknąć uszkodzenia istniejącego wcześniej kodu i być bardziej spójnym.
Gnagno,

3
Jak zauważa rozwiązanie @ Minimal, Class.skip_callbackwywołanie będzie trwałe w innych testach, więc jeśli inne testy oczekują wystąpienia wywołania zwrotnego, zakończą się niepowodzeniem, jeśli spróbujesz odwrócić logikę pominięcia wywołania zwrotnego.
mpdaugherty

Skończyło się na tym, że użyłem odpowiedzi @ uberllama na temat karczowania z Mokką w after(:build)bloku. Umożliwia to fabryczne uruchomienie oddzwaniania i nie wymaga resetowania oddzwaniania po każdym użyciu.
mpdaugherty

Czy myślisz, że to zadziała w inny sposób? stackoverflow.com/questions/35950470/…
Chris Hough

90

Jeśli nie chcesz uruchamiać wywołania zwrotnego, wykonaj następujące czynności:

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

Pamiętaj, że funkcja skip_callback będzie trwała w innych specyfikacjach po uruchomieniu, dlatego rozważ coś takiego:

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

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

12
Bardziej podoba mi się ta odpowiedź, ponieważ wyraźnie stwierdza, że ​​pomijanie wywołań zwrotnych wisi na poziomie klasy i dlatego będzie pomijało wywołania zwrotne w kolejnych testach.
siannopollo

Też mi się to bardziej podoba. Nie chcę, aby moja fabryka na stałe zachowywała się inaczej. Chcę go pominąć dla określonego zestawu testów.
theUtherSide

39

Żadne z tych rozwiązań nie jest dobre. Niszczą klasę, usuwając funkcje, które powinny zostać usunięte z instancji, a nie z klasy.

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

Zamiast blokować wywołanie zwrotne, pomijam jego funkcjonalność. W pewnym sensie bardziej podoba mi się to podejście, ponieważ jest bardziej wyraźne.


1
Naprawdę podoba mi się ta odpowiedź i zastanawiam się, czy coś takiego, z aliasem, aby intencja była natychmiast jasna, powinna być częścią samej FactoryGirl.
Giuseppe,

Ta odpowiedź też mi się podoba tak bardzo, że przegłosowałbym wszystko inne, ale wygląda na to, że musimy przekazać blok do zdefiniowanej metody, jeśli to twój callback jest pokrewny around_*(np user.define_singleton_method(:around_callback_method){|&b| b.call }.).
Quv

1
Nie tylko lepsze rozwiązanie, ale z jakiegoś powodu inna metoda nie zadziałała. Kiedy go zaimplementowałem, powiedział, że nie istnieje żadna metoda wywołania zwrotnego, ale kiedy ją zostawiłem, poprosiłoby mnie o skasowanie niepotrzebnych żądań. Chociaż prowadzę mnie do rozwiązania, czy ktoś wie, dlaczego tak się dzieje?
Babbz77

27

Chciałbym ulepszyć odpowiedź @luizbranco, aby wywołanie zwrotne after_save było bardziej użyteczne podczas tworzenia innych użytkowników.

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

Działa bez funkcji zwrotnej after_save:

FactoryGirl.create(:user)

Uruchamianie z wywołaniem zwrotnym after_save:

FactoryGirl.create(:user, :with_after_save_callback)

W moim teście wolę domyślnie tworzyć użytkowników bez wywołania zwrotnego, ponieważ używane metody uruchamiają dodatkowe rzeczy, których normalnie nie chcę w moich przykładach testowych.

---------- AKTUALIZACJA ------------ Przestałem używać skip_callback, ponieważ wystąpiły problemy z niespójnością w zestawie testów.

Alternatywne rozwiązanie 1 (użycie stub i unstub):

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

Alternatywne rozwiązanie 2 (moje preferowane podejście):

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

Czy myślisz, że to zadziała w inny sposób? stackoverflow.com/questions/35950470/…
Chris Hough

RuboCop narzeka na „Style / SingleLineMethods: unikaj definicji metod jednowierszowych” dla alternatywnego rozwiązania 2, więc będę musiał zmienić formatowanie, ale poza tym jest idealne!
coberlin

15

Rails 5 - skip_callbackpodnoszenie błędu argumentu podczas przeskakiwania z fabryki FactoryBot.

ArgumentError: After commit callback :whatever_callback has not been defined

Nastąpiła zmiana w Railsach 5, w jaki sposób skip_callback obsługuje nierozpoznane wywołania zwrotne:

ActiveSupport :: Callbacks # skip_callback teraz wywołuje ArgumentError, jeśli nierozpoznane wywołanie zwrotne zostanie usunięte

W przypadku skip_callbackwywołania z fabryki rzeczywisty callback w modelu AR nie jest jeszcze zdefiniowany.

Jeśli spróbowałeś wszystkiego i wyrwałeś sobie włosy jak ja, oto twoje rozwiązanie (wziąłem je z wyszukiwania problemów z FactoryBotem) ( UWAGA na raise: falseczęść ):

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

Możesz go używać z dowolnymi innymi strategiami, które preferujesz.


1
Świetnie, właśnie to mi się przydarzyło. Zwróć uwagę, że jeśli raz usunąłeś wywołanie zwrotne i spróbujesz go ponownie, dzieje się tak, więc jest całkiem prawdopodobne, że zostanie to wyzwolone wiele razy dla fabryki.
slhck

6

To rozwiązanie działa dla mnie i nie musisz dodawać dodatkowego bloku do definicji fabryki:

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

Ważna uwaga , należy określić oba z nich. Jeśli użyjesz tylko wcześniej i uruchomisz wiele specyfikacji, spróbuje wyłączyć wywołanie zwrotne wiele razy. Za pierwszym razem się powiedzie, ale za drugim callback nie będzie już definiowany. Więc się pomylisz


Spowodowało to pewne niejasne awarie w pakiecie w ostatnim projekcie - miałem coś podobnego do odpowiedzi @ Sairam, ale wywołanie zwrotne nie zostało ustawione w klasie między testami. Ups.
kfrz

5

W Rspec 3 najlepiej sprawdził się prosty kod

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

5
Trzeba by ustawić go do wystąpień o User; :run_somethingnie jest metodą klasową.
PJSCopeland

4

Dzwonienie do skip_callback z mojej fabryki okazało się dla mnie problematyczne.

W moim przypadku mam klasę dokumentu z niektórymi wywołaniami zwrotnymi związanymi z s3 przed i po utworzeniu, które chcę uruchomić tylko wtedy, gdy konieczne jest przetestowanie pełnego stosu. W przeciwnym razie chcę pominąć te wywołania zwrotne s3.

Kiedy próbowałem skip_callbacks w mojej fabryce, utrzymywało się, że wywołanie zwrotne pomijało nawet wtedy, gdy utworzyłem obiekt dokumentu bezpośrednio, bez użycia fabryki. Zamiast tego użyłem odcinków mokki w wywołaniu po kompilacji i wszystko działa idealnie:

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

Wszystkich tutaj rozwiązań, a za to, że logika w fabryce, jest jedynym, który pracuje z before_validationhakiem (starając się zrobić skip_callbackz dowolnego FactoryGirl użytkownika beforelub afterwariantów buildi createnie działa)
Mike T

3

Będzie to działać z aktualną składnią rspec (od tego postu) i jest znacznie czystsze:

before do
   User.any_instance.stub :run_something
end

jest to przestarzałe w Rspec 3. Używanie zwykłego kodu pośredniczącego zadziałało dla mnie, zobacz moją odpowiedź poniżej.
samg

3

Odpowiedź Jamesa Chevaliera na temat tego, jak pominąć wywołanie zwrotne before_validation nie pomogła mi, więc jeśli masz to samo co ja, tutaj działa rozwiązanie:

w modelu:

before_validation :run_something, on: :create

w fabryce:

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

2
Myślę, że lepiej tego unikać. Pomija wywołania zwrotne dla każdej instancji klasy (nie tylko dla tych wygenerowanych przez factory girl). Doprowadzi to do pewnych problemów z wykonywaniem specyfikacji (np. Jeśli wyłączenie nastąpi po zbudowaniu początkowej fabryki), które mogą być trudne do debugowania. Jeśli jest to pożądane zachowanie w specyfikacji / wsparciu, należy to zrobić wyraźnie: Model.skip_callback(...)
Kevin Sylvestre

2

W moim przypadku mam wywołanie zwrotne ładujące coś do mojej pamięci podręcznej Redis. Ale potem nie miałem / nie chciałem, aby instancja redis działała w moim środowisku testowym.

after_create :load_to_cache

def load_to_cache
  Redis.load_to_cache
end

W mojej sytuacji, podobnie jak powyżej, po prostu zablokowałem moją load_to_cachemetodę w moim spec_helper, z:

Redis.stub(:load_to_cache)

Ponadto w pewnej sytuacji, w której chcę to przetestować, muszę po prostu odblokować je w bloku before odpowiednich przypadków testowych Rspec.

Wiem, że możesz mieć coś bardziej skomplikowanego w twoim after_createżyciu lub może nie być to zbyt eleganckie. Możesz spróbować anulować wywołanie zwrotne zdefiniowane w twoim modelu, definiując after_createhook w swoim Factory (patrz dokumentacja factory_girl), gdzie prawdopodobnie możesz zdefiniować to samo callback i return false, zgodnie z sekcją „Canceling callback” tego artykułu . (Nie jestem pewien, w jakiej kolejności wykonywane są wywołania zwrotne, dlatego nie wybrałem tej opcji).

Na koniec (przepraszam, że nie mogę znaleźć tego artykułu) Ruby pozwala ci użyć jakiegoś brudnego metaprogramowania do odczepienia połączenia zwrotnego (będziesz musiał go zresetować). Myślę, że byłaby to najmniej preferowana opcja.

Cóż, jest jeszcze jedna rzecz, nie do końca rozwiązanie, ale zobacz, czy możesz uciec z Factory.build w swoich specyfikacjach, zamiast faktycznie tworzyć obiekt. (Byłoby to najprostsze, gdybyś mógł).


2

Odnośnie odpowiedzi zamieszczonej powyżej, https://stackoverflow.com/a/35562805/2001785 , nie musisz dodawać kodu do fabryki. Zauważyłem, że łatwiej jest przeładować metody w samych specyfikacjach. Na przykład zamiast (w połączeniu z kodem fabrycznym w cytowanym poście)

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

Lubię używać (bez cytowanego kodu fabrycznego)

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

W ten sposób nie musisz patrzeć zarówno na pliki fabryczne, jak i na pliki testowe, aby zrozumieć zachowanie testu.


1

Zauważyłem, że poniższe rozwiązanie jest bardziej przejrzyste, ponieważ wywołanie zwrotne jest uruchamiane / ustawiane na poziomie klasy.

# 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

Oto fragment utworzony przeze mnie, aby poradzić sobie z tym w ogólny sposób.
Pominie wszystkie skonfigurowane wywołania zwrotne, w tym wywołania zwrotne związane z railsami, takie jak before_save_collection_association, ale nie pominie niektórych potrzebnych do poprawnego działania ActiveRecord, na przykład autosave_associated_records_for_wywołań zwrotnych generowanych automatycznie .

# 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

potem później:

create(:user, :skip_all_callbacks)

Nie trzeba dodawać, YMMV, więc spójrz w dziennikach testów, co tak naprawdę pomijasz. Może masz klejnot, dodając oddzwonienie, którego naprawdę potrzebujesz, i sprawi, że twoje testy będą nieudane lub z twojego modelu tłuszczu 100 wywołań zwrotnych potrzebujesz tylko pary do konkretnego testu. W takich przypadkach wypróbuj przejściowy:force_callbacks

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

PREMIA

Czasami musisz również pominąć walidację (wszystko po to, aby testy były szybsze), a następnie spróbuj z:

  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

Możesz po prostu ustawić wywołanie zwrotne z cechą dla tych instancji, kiedy chcesz je uruchomić.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.