Ignorer les rappels sur Factory Girl et Rspec


103

Je teste un modèle avec un rappel après création que je voudrais exécuter uniquement à certaines occasions lors du test. 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

Réponses:


111

Je ne suis pas sûr que ce soit la meilleure solution, mais j'ai réussi à y parvenir 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

Exécution sans rappel:

FactoryGirl.create(:user)

Exécution avec rappel:

FactoryGirl.create(:user_with_run_something)

3
Si vous voulez sauter une :on => :createvalidation, utilisezafter(:build) { |user| user.class.skip_callback(:validate, :create, :after, :run_something) }
James Chevalier

8
ne serait-il pas préférable d'inverser la logique de rappel de saut? Je veux dire, la valeur par défaut devrait être que lorsque je crée un objet, les rappels sont déclenchés, et je devrais utiliser un paramètre différent pour le cas exceptionnel. donc FactoryGirl.create (: user) devrait créer l'utilisateur déclenchant les callbacks, et FactoryGirl.create (: user_without_callbacks) devrait créer l'utilisateur sans les callbacks. Je sais que ce n'est qu'une modification de "conception", mais je pense que cela peut éviter de casser le code préexistant et être plus cohérent.
Gnagno

3
Comme le note la solution de @ Minimal, l' Class.skip_callbackappel sera persistant dans d'autres tests, donc si vos autres tests s'attendent à ce que le rappel se produise, ils échoueront si vous essayez d'inverser la logique de rappel de saut.
mpdaugherty

J'ai fini par utiliser la réponse de @ uberllama sur le stubbing avec Mocha dans le after(:build)bloc. Cela permet à votre usine d'exécuter le rappel par défaut et ne nécessite pas de réinitialiser le rappel après chaque utilisation.
mpdaugherty

Pensez-vous que cela fonctionne dans l'autre sens? stackoverflow.com/questions/35950470/…
Chris Hough

90

Lorsque 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, considérez donc quelque chose comme ce qui suit:

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

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

12
J'aime mieux cette réponse car elle indique explicitement que sauter les rappels se déroule au niveau de la classe et continuerait donc à ignorer les rappels dans les tests suivants.
siannopollo

J'aime mieux ça aussi. Je ne veux pas que mon usine se comporte différemment en permanence. Je veux l'ignorer pour un ensemble particulier de tests.
theUtherSide

39

Aucune de ces solutions n'est bonne. Ils dégradent la classe en supprimant les fonctionnalités qui doivent être supprimées de l'instance, pas 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.


1
J'aime vraiment cette réponse et je me demande si quelque chose comme ça, aliasé pour que l'intention soit immédiatement claire, devrait faire partie de FactoryGirl lui-même.
Giuseppe

J'aime aussi tellement cette réponse que je voterais contre tout le reste, mais il semble que nous devions passer un bloc à la méthode définie, si c'est votre rappel est le parent de around_*(par exemple user.define_singleton_method(:around_callback_method){|&b| b.call }).
Quv

1
Non seulement une meilleure solution, mais pour une raison quelconque, l'autre méthode n'a pas fonctionné pour moi. Quand je l'ai implémenté, il a dit qu'aucune méthode de rappel n'existait, mais quand je l'ai laissée de côté, cela me demandait de stuber les demandes inutiles. Bien que cela m'amène à une solution, est-ce que quelqu'un sait pourquoi cela pourrait être?
Babbz77

27

J'aimerais apporter une amélioration à la réponse de @luizbranco pour 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)

Exécution avec le rappel after_save:

FactoryGirl.create(:user, :with_after_save_callback)

Dans mon test, je préfère créer des utilisateurs sans le rappel par défaut car les méthodes utilisées exécutent des éléments supplémentaires que je ne veux normalement pas dans mes exemples de test.

---------- MISE À JOUR ------------ J'ai arrêté d'utiliser skip_callback car il y avait des 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

Solution alternative 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

Pensez-vous que cela fonctionne dans l'autre sens? stackoverflow.com/questions/35950470/…
Chris Hough

RuboCop se plaint de "Style / SingleLineMethods: Évitez les définitions de méthode sur une seule ligne" pour la solution alternative 2, donc je vais devoir changer la mise en forme, mais sinon c'est parfait!
coberlin

15

Rails 5 - skip_callbackerreur d'argument de levée lors du saut d'une usine 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 gère les rappels non reconnus:

ActiveSupport :: Callbacks # skip_callback déclenche maintenant une ArgumentError si un rappel non reconnu est supprimé

Lorsqu'il skip_callbackest appelé depuis l'usine, le véritable rappel dans le modèle AR n'est pas encore défini.

Si vous avez tout essayé et que vous vous êtes arraché les cheveux comme moi, voici votre solution (obtenue en recherchant des problèmes FactoryBot) ( NOTEZ la raise: falsepartie ):

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

N'hésitez pas à l'utiliser avec les autres stratégies que vous préférez.


1
Super, c'est exactement ce qui m'est arrivé. Notez que si vous avez supprimé un rappel une fois et réessayez, cela se produit, il est donc fort probable que cela se déclenche plusieurs fois pour une usine.
slhck le

6

Cette solution fonctionne pour moi et vous n'avez pas besoin d'ajouter un bloc supplémentaire à votre définition d'usine:

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

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

5

Un simple talon fonctionnait mieux pour moi dans Rspec 3

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

5
Vous devez le configurer pour les instances de User; :run_somethingn'est pas une méthode de classe.
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

Remarque importante, vous devez spécifier les deux. Si vous utilisez uniquement avant et exécutez plusieurs spécifications, il essaiera de désactiver le rappel plusieurs fois. Cela réussira la première fois, mais la seconde, le rappel ne sera plus défini. Donc ça va faire une erreur


Cela a provoqué des échecs obscurs dans une suite sur un projet récent - j'avais quelque chose de similaire à la réponse de @ Sairam mais le rappel n'était pas défini dans la classe entre les tests. Oups.
kfrz

4

L'appel de 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 ne veux exécuter que 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, cela a persisté dans ce saut de rappel même lorsque j'ai créé un objet document directement, sans utiliser de fabrique. Donc à la place, j'ai utilisé des stubs moka dans l'appel après la 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

De toutes les solutions ici, et pour avoir la logique au sein de l'usine, c'est la seule qui fonctionne avec un before_validationcrochet (essayant de faire skip_callbackavec l'une des FactoryGirl beforeou des afteroptions pour buildet createn'a pas fonctionné)
Mike T

3

Cela fonctionnera avec la syntaxe rspec actuelle (à partir de ce post) et est beaucoup plus propre:

before do
   User.any_instance.stub :run_something
end

ceci est obsolète dans Rspec 3. L'utilisation d'un stub régulier a fonctionné pour moi, voir ma réponse ci-dessous.
samg

3

La réponse de James Chevalier sur la façon d'ignorer le rappel before_validation ne m'a pas aidé, donc si vous traitez comme moi, voici une solution de travail:

dans le modèle:

before_validation :run_something, on: :create

en usine:

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

2
Je pense qu'il est préférable d'éviter cela. Il ignore les rappels pour chaque instance de la classe (pas seulement celles générées par factory girl). Cela entraînera des problèmes d'exécution de spécifications (c'est-à-dire si la désactivation se produit après la construction de l'usine initiale) qui peuvent être difficiles à déboguer. Si tel est le comportement souhaité dans la spécification / le support, il doit être fait explicitement: Model.skip_callback(...)
Kevin Sylvestre

2

Dans mon cas, j'ai le rappel qui charge quelque chose dans mon cache redis. Mais alors je n'avais pas / je ne voulais pas qu'une instance redis s'exécute pour mon environnement de test.

after_create :load_to_cache

def load_to_cache
  Redis.load_to_cache
end

Pour ma situation, similaire à celle ci-dessus, j'ai juste stubé ma load_to_cacheméthode dans mon spec_helper, avec:

Redis.stub(:load_to_cache)

De plus, dans certaines situations où je veux tester cela, je dois juste les décoller dans le bloc avant des cas de test Rspec correspondants.

Je sais que vous pourriez avoir quelque chose de plus compliqué qui se passe after_createou que vous ne trouverez pas cela très élégant. Vous pouvez essayer d'annuler le rappel défini dans votre modèle, en définissant un after_createhook dans votre Factory (reportez-vous à la documentation de factory_girl), où vous pouvez probablement définir un même rappel et retour false, selon la section `` Annulation des rappels '' de cet article . (Je ne suis pas sûr de l'ordre dans lequel les rappels sont exécutés, c'est pourquoi je n'ai pas choisi cette option).

Enfin, (désolé je ne trouve pas 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. (Ce serait le plus simple si vous le pouvez).


2

Concernant la réponse postée ci-dessus, https://stackoverflow.com/a/35562805/2001785 , vous n'avez pas besoin d'ajouter le code à l'usine. 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 de regarder à la fois l'usine et les fichiers de test pour comprendre le comportement du test.


1

J'ai trouvé que la solution suivante était un moyen 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

0

Voici un extrait que j'ai créé pour gérer cela de manière générique.
Il ignorera tous les rappels configurés, y compris les rappels liés aux rails comme before_save_collection_association, mais il n'en sautera pas certains nécessaires pour que ActiveRecord fonctionne correctement, comme les autosave_associated_records_for_rappels générés automatiquement .

# 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

puis plus tard:

create(:user, :skip_all_callbacks)

Inutile de dire, YMMV, alors regardez dans les journaux de test ce que vous sautez vraiment. Peut-être avez-vous un petit bijou qui ajoute un rappel dont vous avez vraiment besoin et que vos tests échouent lamentablement ou à partir de votre modèle de 100 rappels, vous avez juste besoin d'un couple pour un test spécifique. Pour ces cas, essayez le transitoire:force_callbacks

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

PRIME

Parfois, vous devez également ignorer les validations (le tout dans un effort pour accélérer les tests), puis essayez avec:

  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

Vous pouvez simplement définir le rappel avec un trait pour ces instances lorsque vous souhaitez l'exécuter.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.