Überspringen Sie Rückrufe bei Factory Girl und Rspec

103

Ich teste ein Modell mit einem After-Create-Rückruf, den ich beim Testen nur gelegentlich ausführen möchte. Wie kann ich Rückrufe von einer Fabrik aus überspringen / ausführen?

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

Fabrik:

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

    factory :with_run_something do
      # run callback
  end
end
luizbranco
quelle

Antworten:

111

Ich bin nicht sicher, ob es die beste Lösung ist, aber ich habe dies erfolgreich erreicht mit:

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

Laufen ohne Rückruf:

FactoryGirl.create(:user)

Laufen mit Rückruf:

FactoryGirl.create(:user_with_run_something)
luizbranco
quelle
3
Wenn Sie eine :on => :createValidierung überspringen möchten , verwenden Sieafter(:build) { |user| user.class.skip_callback(:validate, :create, :after, :run_something) }
James Chevalier
7
Wäre es nicht besser, die überspringende Rückruflogik umzukehren? Ich meine, die Standardeinstellung sollte sein, dass beim Erstellen eines Objekts die Rückrufe ausgelöst werden, und ich sollte für den Ausnahmefall einen anderen Parameter verwenden. Daher sollte FactoryGirl.create (: user) den Benutzer erstellen, der die Rückrufe auslöst, und FactoryGirl.create (: user_without_callbacks) sollte den Benutzer ohne die Rückrufe erstellen. Ich weiß, dass dies nur eine "Design" -Modifikation ist, aber ich denke, dies kann vermeiden, dass bereits vorhandener Code beschädigt wird, und konsistenter sein.
Gnagno
3
Wie in der Lösungsnotiz von @ Minimal angegeben, Class.skip_callbackbleibt der Anruf bei anderen Tests bestehen. Wenn Ihre anderen Tests also den Rückruf erwarten, schlagen sie fehl, wenn Sie versuchen, die überspringende Rückruflogik zu invertieren.
mpdaugherty
Am Ende habe ich @ uberllamas Antwort über das Stubben mit Mokka im after(:build)Block verwendet. Dadurch kann Ihre Werkseinstellung den Rückruf standardmäßig ausführen und muss den Rückruf nicht nach jeder Verwendung zurücksetzen.
mpdaugherty
Haben Sie irgendwelche Gedanken darüber, dass dies anders funktioniert? stackoverflow.com/questions/35950470/…
Chris Hough
89

Wenn Sie keinen Rückruf ausführen möchten, gehen Sie wie folgt vor:

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

Beachten Sie, dass skip_callback nach der Ausführung über andere Spezifikationen hinweg bestehen bleibt. Beachten Sie daher Folgendes:

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

after do
  User.set_callback(:create, :after, :run_something)
end
Minimul
quelle
12
Ich mag diese Antwort besser, weil sie ausdrücklich besagt, dass das Überspringen von Rückrufen auf Klassenebene herumhängt und daher Rückrufe in nachfolgenden Tests weiterhin überspringen würde.
Siannopollo
Das gefällt mir auch besser. Ich möchte nicht, dass sich meine Fabrik dauerhaft anders verhält. Ich möchte es für eine bestimmte Reihe von Tests überspringen.
theUtherSide
39

Keine dieser Lösungen ist gut. Sie verunstalten die Klasse, indem sie Funktionen entfernen, die aus der Instanz und nicht aus der Klasse entfernt werden sollen.

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

Anstatt den Rückruf zu unterdrücken, unterdrücke ich die Funktionalität des Rückrufs. In gewisser Weise gefällt mir dieser Ansatz besser, weil er expliziter ist.

B Sieben
quelle
1
Ich mag diese Antwort wirklich und frage mich, ob so etwas, das so voreingenommen ist, dass die Absicht sofort klar ist, Teil von FactoryGirl selbst sein sollte.
Giuseppe
Ich mag diese Antwort auch so sehr, dass ich alles andere ablehnen würde, aber es scheint, dass wir einen Block an die definierte Methode übergeben müssen, wenn es Ihr Rückruf ist, der verwandt ist mit around_*(z user.define_singleton_method(:around_callback_method){|&b| b.call }. B. ).
Quv
1
Nicht nur eine bessere Lösung, sondern aus irgendeinem Grund hat die andere Methode bei mir nicht funktioniert. Als ich es implementierte, hieß es, dass es keine Rückrufmethode gibt, aber wenn ich es weglasse, würde es mich bitten, die unnötigen Anfragen zu stubben. Obwohl ich zu einer Lösung geführt habe, weiß jemand, warum das so sein könnte?
Babbz77
26

Ich möchte die Antwort von @luizbranco verbessern, um den Rückruf nach dem Speichern beim Erstellen anderer Benutzer wiederverwendbarer zu machen.

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

Laufen ohne after_save-Rückruf:

FactoryGirl.create(:user)

Ausführen mit after_save-Rückruf:

FactoryGirl.create(:user, :with_after_save_callback)

In meinem Test ziehe ich es vor, Benutzer standardmäßig ohne Rückruf zu erstellen, da die verwendeten Methoden zusätzliche Dinge ausführen, die ich normalerweise in meinen Testbeispielen nicht möchte.

---------- UPDATE ------------ Ich habe die Verwendung von skip_callback eingestellt, da in der Testsuite einige Inkonsistenzprobleme aufgetreten sind.

Alternative Lösung 1 (Verwendung von Stub und 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

Alternative Lösung 2 (mein bevorzugter Ansatz):

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
konyak
quelle
Haben Sie irgendwelche Gedanken darüber, dass dies anders funktioniert? stackoverflow.com/questions/35950470/…
Chris Hough
RuboCop beschwert sich mit "Style / SingleLineMethods: Vermeiden Sie einzeilige Methodendefinitionen" für Alternative Solution 2, daher muss ich die Formatierung ändern, aber ansonsten ist es perfekt!
Coberlin
14

Rails 5 - skip_callbackAuslösen eines Argumentfehlers beim Überspringen von einer FactoryBot-Factory.

ArgumentError: After commit callback :whatever_callback has not been defined

Es gab eine Änderung in Rails 5 mit wie skip_callback Griffe unerkannt Rückrufe:

ActiveSupport :: Callbacks # skip_callback löst jetzt einen ArgumentError aus, wenn ein nicht erkannter Rückruf entfernt wird

Wenn skip_callbackab Werk aufgerufen wird, ist der tatsächliche Rückruf im AR-Modell noch nicht definiert.

Wenn Sie alles ausprobiert und sich wie ich die Haare ausgezogen haben, finden Sie hier Ihre Lösung (aus der Suche nach FactoryBot-Problemen) ( HINWEIS zum raise: falseTeil ):

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

Fühlen Sie sich frei, es mit anderen Strategien zu verwenden, die Sie bevorzugen.

RudyOnRails
quelle
1
Großartig, genau das ist mir passiert. Beachten Sie, dass dies passiert, wenn Sie einen Rückruf einmal entfernt und erneut versucht haben. Daher ist es sehr wahrscheinlich, dass dies für eine Fabrik mehrmals ausgelöst wird.
Slhck
6

Diese Lösung funktioniert für mich und Sie müssen Ihrer Factory-Definition keinen zusätzlichen Block hinzufügen:

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

user = FactoryGirl.create(:user)     # Execute callbacks
Auralbee
quelle
5

Ein einfacher Stub hat in Rspec 3 am besten für mich funktioniert

allow(User).to receive_messages(:run_something => nil)
Samg
quelle
4
Sie müssten es für Instanzen von einrichten User; :run_somethingist keine Klassenmethode.
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

Wichtiger Hinweis, Sie sollten beide angeben. Wenn Sie nur vorher verwenden und mehrere Spezifikationen ausführen, wird versucht, den Rückruf mehrmals zu deaktivieren. Es wird beim ersten Mal erfolgreich sein, aber beim zweiten Mal wird kein Rückruf mehr definiert. Also wird es einen Fehler geben

AndreiMotinga
quelle
Dies führte bei einem kürzlich durchgeführten Projekt zu einigen verschleierten Fehlern in einer Suite. Ich hatte etwas Ähnliches wie die Antwort von @ Sairam, aber der Rückruf wurde in der Klasse zwischen den Tests nicht gesetzt. Hoppla.
kfrz
4

Das Aufrufen von skip_callback aus meiner Fabrik erwies sich für mich als problematisch.

In meinem Fall habe ich eine Dokumentklasse mit einigen s3-bezogenen Rückrufen vor und nach dem Erstellen, die ich nur ausführen möchte, wenn das Testen des vollständigen Stapels erforderlich ist. Ansonsten möchte ich diese s3-Rückrufe überspringen.

Als ich in meiner Factory übersprang_callbacks versuchte, blieb dieser Rückrufsprung bestehen, selbst wenn ich ein Dokumentobjekt direkt ohne Verwendung einer Factory erstellt habe. Also habe ich stattdessen Mokka-Stubs im After-Build-Aufruf verwendet und alles funktioniert perfekt:

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
Uberllama
quelle
Von allen Lösungen hier und für die Logik innerhalb der Fabrik ist dies die einzige, die mit einem before_validationHaken arbeitet (versucht, skip_callbackmit einer der FactoryGirls beforeoder afterOptionen für zu arbeiten buildund createhat nicht funktioniert)
Mike T
3

Dies funktioniert mit der aktuellen rspec-Syntax (ab diesem Beitrag) und ist viel sauberer:

before do
   User.any_instance.stub :run_something
end
Zyren
quelle
Dies ist in Rspec 3 veraltet. Verwenden Sie einen normalen Stub, der für mich funktioniert hat, siehe meine Antwort unten.
Samg
3

Die Antwort von James Chevalier, wie man den Rückruf vor_validierung überspringt, hat mir nicht geholfen. Wenn Sie also genauso wie ich zappeln, ist dies eine funktionierende Lösung:

im Modell:

before_validation :run_something, on: :create

in der Fabrik:

after(:build) { |obj| obj.class.skip_callback(:validation, :before, :run_something) }
Tetiana Chupryna
quelle
2
Ich denke, es ist vorzuziehen, dies zu vermeiden. Es werden Rückrufe für jede Instanz der Klasse übersprungen (nicht nur für diejenigen, die vom Fabrikmädchen generiert wurden). Dies führt zu einigen Problemen bei der Ausführung von Spezifikationen (z. B. wenn die Deaktivierung nach dem Erstellen der ersten Factory erfolgt), die möglicherweise nur schwer zu debuggen sind. Wenn dies das gewünschte Verhalten in der Spezifikation / Unterstützung ist, sollte dies explizit erfolgen: Model.skip_callback(...)
Kevin Sylvestre
2

In meinem Fall lädt der Rückruf etwas in meinen Redis-Cache. Aber dann hatte / wollte ich keine Redis-Instanz für meine Testumgebung.

after_create :load_to_cache

def load_to_cache
  Redis.load_to_cache
end

Für meine Situation, ähnlich wie oben, habe ich meine load_to_cacheMethode in meinem spec_helper mit folgenden Schritten gestoppt:

Redis.stub(:load_to_cache)

In bestimmten Situationen, in denen ich dies testen möchte, muss ich sie nur im Vorher-Block der entsprechenden Rspec-Testfälle entfernen.

Ich weiß, dass in Ihnen möglicherweise etwas Komplizierteres passiert after_createoder dass Sie dies nicht sehr elegant finden. Sie können versuchen, den in Ihrem Modell definierten Rückruf abzubrechen, indem Sie after_createin Ihrer Factory einen Hook definieren (siehe factory_girl-Dokumente), in dem Sie wahrscheinlich denselben Rückruf definieren und zurückgeben können false, wie im Abschnitt "Rückrufe abbrechen" dieses Artikels beschrieben . (Ich bin mir nicht sicher, in welcher Reihenfolge der Rückruf ausgeführt wird, weshalb ich mich nicht für diese Option entschieden habe.)

Schließlich (Entschuldigung, ich kann den Artikel nicht finden) können Sie mit Ruby eine schmutzige Metaprogrammierung verwenden, um einen Rückruf-Hook zu lösen (Sie müssen ihn zurücksetzen). Ich denke, dies wäre die am wenigsten bevorzugte Option.

Nun, es gibt noch eine Sache, nicht wirklich eine Lösung, aber sehen Sie, ob Sie mit Factory.build in Ihren Spezifikationen davonkommen können, anstatt das Objekt tatsächlich zu erstellen. (Wäre das einfachste wenn du kannst).

jake
quelle
2

In Bezug auf die oben angegebene Antwort https://stackoverflow.com/a/35562805/2001785 müssen Sie den Code nicht zur Fabrik hinzufügen. Ich fand es einfacher, die Methoden in den Spezifikationen selbst zu überladen. Zum Beispiel anstelle von (in Verbindung mit dem Fabrikcode im zitierten Beitrag)

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

Ich benutze gerne (ohne den angegebenen Fabrikcode)

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

Auf diese Weise müssen Sie nicht sowohl die Werks- als auch die Testdateien betrachten, um das Verhalten des Tests zu verstehen.

bhfailor
quelle
1

Ich fand die folgende Lösung sauberer, da der Rückruf auf Klassenebene ausgeführt / festgelegt wird.

# 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
Sairam
quelle
0

Hier ist ein Ausschnitt, den ich erstellt habe, um dies generisch zu handhaben.
Es wird jeder konfigurierte Rückruf übersprungen, einschließlich railbezogener Rückrufe wie before_save_collection_association, aber es werden nicht einige übersprungen, die erforderlich sind, damit ActiveRecord funktioniert, wie automatisch generierte autosave_associated_records_for_Rückrufe.

# 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

dann später:

create(:user, :skip_all_callbacks)

Unnötig zu sagen, YMMV, schauen Sie also in den Testprotokollen nach, was Sie wirklich überspringen. Vielleicht haben Sie einen Edelstein, der einen Rückruf hinzufügt, den Sie wirklich brauchen, und der dazu führt, dass Ihre Tests kläglich scheitern, oder von Ihrem Fettmodell mit 100 Rückrufen benötigen Sie nur ein Paar für einen bestimmten Test. Versuchen Sie in diesen Fällen den Transienten:force_callbacks

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

BONUS

Manchmal müssen Sie auch Validierungen überspringen (um Tests schneller zu machen) und dann versuchen:

  trait :skip_validate do
    to_create { |instance| instance.save(validate: false) }
  end
Alter Lagos
quelle
-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

Sie können den Rückruf einfach mit einem Merkmal für diese Instanzen festlegen, wenn Sie ihn ausführen möchten.

user6520080
quelle