Je teste un modèle avec un rappel après création que je souhaiterais exécuter uniquement lors de tests. Comment puis-je ignorer/exécuter des rappels depuis une usine?
class User < ActiveRecord::Base
after_create :run_something
...
end
Usine:
FactoryGirl.define do
factory :user do
first_name "Luiz"
last_name "Branco"
...
# skip callback
factory :with_run_something do
# run callback
end
end
Je ne sais pas si c'est la meilleure solution, mais j'ai réussi à le faire en utilisant:
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
En cours d'exécution sans rappel:
FactoryGirl.create(:user)
Courir avec callback:
FactoryGirl.create(:user_with_run_something)
Si vous ne souhaitez pas exécuter de rappel, procédez comme suit:
User.skip_callback(:create, :after, :run_something)
Factory.create(:user)
Sachez que skip_callback sera persistant dans les autres spécifications après son exécution. Par conséquent, tenez compte des éléments suivants:
before do
User.skip_callback(:create, :after, :run_something)
end
after do
User.set_callback(:create, :after, :run_something)
end
Aucune de ces solutions ne sont bonnes. Ils dégradent la classe en supprimant les fonctionnalités qui doivent être supprimées de l'instance et non de la classe.
factory :user do
before(:create){|user| user.define_singleton_method(:send_welcome_email){}}
Au lieu de supprimer le rappel, je supprime la fonctionnalité du rappel. D'une certaine manière, j'aime mieux cette approche car elle est plus explicite.
J'aimerais apporter une amélioration à la réponse de @luizbranco afin de rendre le rappel after_save plus réutilisable lors de la création d'autres utilisateurs.
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
Exécution sans rappel after_save:
FactoryGirl.create(:user)
En cours d'exécution avec callback after_save:
FactoryGirl.create(:user, :with_after_save_callback)
Dans mon test, je préfère créer des utilisateurs sans rappel par défaut, car les méthodes utilisées exécutent des tâches supplémentaires que je ne souhaite pas normalement dans mes exemples de test.
---------- UPDATE ------------ J'ai arrêté d'utiliser skip_callback en raison de problèmes d'incohérence dans la suite de tests.
Solution alternative 1 (utilisation de stub et 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 Solution 2 (mon approche préférée):
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
Un talon simple a fonctionné le mieux pour moi dans Rspec 3
allow(User).to receive_messages(:run_something => nil)
Cette solution fonctionne pour moi et vous n’aurez pas à ajouter de bloc supplémentaire à votre définition Factory:
user = FactoryGirl.build(:user)
user.send(:create_without_callbacks) # Skip callback
user = FactoryGirl.create(:user) # Execute callbacks
Appeler skip_callback depuis mon usine s'est avéré problématique pour moi.
Dans mon cas, j'ai une classe de document avec des rappels liés à s3 avant et après la création que je veux exécuter uniquement lorsque le test de la pile complète est nécessaire. Sinon, je veux ignorer ces rappels s3.
Lorsque j'ai essayé skip_callbacks dans mon usine, il persistait à ignorer le rappel même lorsque je créais directement un objet document, sans utiliser d'usine. Donc au lieu de cela, j’ai utilisé des talons de moka dans l’appel après construction et tout fonctionne parfaitement:
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
Remarque importante vous devez spécifier les deux. Si seulement utiliser avant et exécuter plusieurs spécifications, il tentera de désactiver le rappel plusieurs fois Cela réussira la première fois, mais la seconde fois, le rappel ne sera plus défini. Donc ça va sortir erreur
Cela fonctionnera avec la syntaxe actuelle de rspec (à partir de cet article) et est beaucoup plus propre:
before do
User.any_instance.stub :run_something
end
La réponse de James Chevalier sur la manière de sauter le rappel avant_validation ne m'a pas aidé, donc si vous échouez comme moi, voici une solution qui fonctionne:
dans le modèle:
before_validation :run_something, on: :create
en usine:
after(:build) { |obj| obj.class.skip_callback(:validation, :before, :run_something) }
En ce qui concerne la réponse affichée ci-dessus, https://stackoverflow.com/a/35562805/2001785 , vous n'avez pas besoin d'ajouter le code à la fabrique. J'ai trouvé plus facile de surcharger les méthodes dans les spécifications elles-mêmes. Par exemple, au lieu de (en conjonction avec le code d'usine dans l'article cité)
let(:user) { FactoryGirl.create(:user) }
J'aime utiliser (sans le code d'usine cité)
let(:user) do
FactoryGirl.build(:user).tap do |u|
u.define_singleton_method(:send_welcome_email){}
u.save!
end
end
end
De cette façon, vous n'avez pas besoin d'examiner à la fois les fichiers d'usine et de test pour comprendre le comportement du test.
Dans mon cas, le rappel charge quelque chose dans mon cache Redis. Mais alors je n'avais pas/je voulais une instance de Redis en cours d'exécution pour mon environnement de test.
after_create :load_to_cache
def load_to_cache
Redis.load_to_cache
end
Pour ma situation, semblable à celle décrite ci-dessus, je viens de décrire ma méthode load_to_cache
dans mon spec_helper, Avec:
Redis.stub(:load_to_cache)
De plus, dans certaines situations où je veux tester ceci, il me suffit de les désosser dans le bloc avant des tests Rspec correspondants.
Je sais que vous avez peut-être quelque chose de plus compliqué dans votre after_create
ou que vous ne trouvez pas cela très élégant. Vous pouvez essayer d'annuler le rappel défini dans votre modèle en définissant un point d'ancrage after_create
dans votre fabrique (voir docs factory_girl), où vous pouvez probablement définir le même rappel et renvoyer false
, conformément à la section 'Annulation des rappels'. article . (Je ne suis pas sûr de l'ordre dans lequel les rappels sont exécutés, c'est pourquoi je ne suis pas parti pour cette option).
Enfin, (désolé je ne parviens pas à trouver l'article) Ruby vous permet d'utiliser une méta-programmation sale pour décrocher un crochet de rappel (vous devrez le réinitialiser). Je suppose que ce serait l'option la moins préférée.
Eh bien, il y a encore une chose, pas vraiment une solution, mais voyez si vous pouvez vous en sortir avec Factory.build dans vos spécifications, au lieu de créer réellement l'objet. (Serait le plus simple si vous le pouvez).
skip_callback
soulevant une erreur d'argument lors du saut depuis une fabrique FactoryBot.ArgumentError: After commit callback :whatever_callback has not been defined
Il y a eu un changement dans Rails 5 avec la façon dont skip_callback traite les rappels non reconnus:
ActiveSupport :: Callbacks # skip_callback génère maintenant une ArgumentError si un rappel non reconnu est supprimé
Lorsque skip_callback
est appelé depuis l'usine, le rappel réel dans le modèle AR n'est pas encore défini.
Si vous avez tout essayé et que vous vous êtes coiffé comme moi, voici la solution (obtenue grâce à la recherche de problèmes FactoryBot) } _ ( NOTEZ le raise: false
part ):
after(:build) { YourSweetModel.skip_callback(:commit, :after, :whatever_callback, raise: false) }
N'hésitez pas à l'utiliser avec les stratégies que vous préférez.
J'ai trouvé la solution suivante une méthode plus propre puisque le rappel est exécuté/défini au niveau de la classe.
# 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