Ich teste ein Modell mit einem After-Create-Callback, das 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
Ich bin nicht sicher, ob es die beste Lösung ist, aber ich habe dies mit folgendem Erfolg erreicht:
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
Läuft ohne Rückruf:
FactoryGirl.create(:user)
Laufen mit Rückruf:
FactoryGirl.create(:user_with_run_something)
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 seiner Ausführung für andere Spezifikationen persistent ist. Beachten Sie daher Folgendes:
before do
User.skip_callback(:create, :after, :run_something)
end
after do
User.set_callback(:create, :after, :run_something)
end
Keine dieser Lösungen ist gut. Sie entstellen die Klasse, indem sie Funktionen entfernen, die aus der Instanz entfernt werden sollten, nicht aus der Klasse.
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.
Ich möchte die Antwort von @luizbranco verbessern, um after_save callback beim Erstellen anderer Benutzer wieder verwendbarer 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
Läuft ohne after_save callback:
FactoryGirl.create(:user)
Läuft mit after_save callback:
FactoryGirl.create(:user, :with_after_save_callback)
In meinem Test ziehe ich es vor, Benutzer standardmäßig ohne Callback zu erstellen, da die verwendeten Methoden zusätzliche Elemente ausführen, die ich normalerweise in meinen Testbeispielen nicht möchte.
---------- UPDATE ------------.__: Ich habe die Verwendung von skip_callback beendet, 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
Ein einfacher Stub hat in Rspec 3 am besten funktioniert
allow(User).to receive_messages(:run_something => nil)
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
Der Aufruf von skip_callback aus meiner Fabrik war für mich problematisch.
In meinem Fall habe ich vor und nach dem Erstellen eine Dokumentenklasse mit einigen s3-bezogenen Callbacks, die ich nur ausführen möchte, wenn der vollständige Stack getestet werden muss. Ansonsten möchte ich die s3-Rückrufe überspringen.
Wenn ich in meiner Factory skip_callbacks ausprobierte, blieb dieser Callback-Sprung bestehen, selbst wenn ich ein Dokumentobjekt direkt erstellt habe, ohne eine Factory zu verwenden. Stattdessen habe ich Mocca-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
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 zuvor nur mehrere Spezifikationen verwenden und ausführen, wird der Rückruf mehrmals deaktiviert. Es wird beim ersten Mal gelingen, beim zweiten wird Callback nicht mehr definiert. Also wird es einen Fehler geben
Dies funktioniert mit der aktuellen rspec-Syntax (ab diesem Beitrag) und ist viel sauberer:
before do
User.any_instance.stub :run_something
end
Die Antwort von James Chevalier über das Überspringen des before_validation-Callbacks hat mir nicht geholfen.
im Modell:
before_validation :run_something, on: :create
in der Fabrik:
after(:build) { |obj| obj.class.skip_callback(:validation, :before, :run_something) }
In Bezug auf die Antwort ( https://stackoverflow.com/a/35562805/2001785 ) müssen Sie den Code nicht zur Factory hinzufügen. Ich fand es einfacher, die Methoden in den Spezifikationen selbst zu überladen. Zum Beispiel anstelle von (in Verbindung mit dem Werkscode in der zitierten Stelle)
let(:user) { FactoryGirl.create(:user) }
Ich mag es (ohne den genannten 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 nur die Werks- und die Testdatei betrachten, um das Verhalten des Tests zu verstehen.
In meinem Fall muss der Callback etwas in meinen Redis-Cache laden. 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 gerade meine load_to_cache
-Methode in meinem spec_helper, Mit:
Redis.stub(:load_to_cache)
In bestimmten Situationen, in denen ich dies testen möchte, muss ich sie lediglich im Vorher-Block der entsprechenden Rspec-Testfälle deaktivieren.
Ich weiß, dass in Ihrem after_create
etwas komplizierteres passiert, oder Sie finden das nicht sehr elegant. Sie können versuchen, den in Ihrem Modell definierten Rückruf abzubrechen, indem Sie in Ihrer Factory einen after_create
-Hook definieren (siehe factory_girl-Dokumente), in dem Sie wahrscheinlich denselben Rückruf definieren und false
zurückgeben können, entsprechend dem Abschnitt 'Callbacks abbrechen' in diesem Abschnitt Artikel . (Ich bin nicht sicher, in welcher Reihenfolge der Rückruf ausgeführt wird, weshalb ich diese Option nicht gewählt habe).
(Leider kann ich den Artikel nicht finden.) Ruby ermöglicht Ihnen die Verwendung einiger Dirty-Meta-Programme, um einen Callback-Hook aufzuheben (Sie müssen ihn zurücksetzen). Ich denke, das wäre die am wenigsten bevorzugte Option.
Nun, es gibt noch eine weitere Sache, nicht wirklich eine Lösung, aber sehen Sie, ob Sie Factory.build in Ihren Spezifikationen verwenden können, anstatt das Objekt tatsächlich zu erstellen. (Wäre am einfachsten, wenn du kannst).
skip_callback
löst beim Überspringen aus einer FactoryBot-Factory einen Argumentfehler aus.ArgumentError: After commit callback :whatever_callback has not been defined
Es gab einen Wechsel in Rails 5 wie skip_callback nicht erkannte Rückrufe behandelt:
ActiveSupport :: Callbacks # skip_callback löst jetzt einen ArgumentError aus, wenn ein nicht erkannter Callback entfernt wird
Wenn skip_callback
ab Werk aufgerufen wird, ist der tatsächliche Rückruf im AR-Modell noch nicht definiert.
Wenn Sie alles ausprobiert und Ihre Haare wie ich herausgezogen haben, finden Sie hier Ihre Lösung (stammt aus der Suche nach FactoryBot-Problemen) ( HINWEIS: raise: false
part ):
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.
Ich habe festgestellt, dass die folgende Lösung sauberer ist, da der Rückruf auf Klassenebene ausgeführt 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