119 votes

Sauter les callbacks sur Factory Girl et Rspec

Je teste un modèle avec une fonction d'appel après création que j'aimerais exécuter uniquement à certaines occasions pendant le test. Comment puis-je ignorer/exécuter les callbacks à partir d'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

116voto

luizbranco Points 785

Je ne suis pas sûr qu'il s'agisse de la meilleure solution, mais j'y suis parvenu en utilisant la méthode suivante :

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 callback :

FactoryGirl.create(:user_with_run_something)

3 votes

Si vous voulez sauter une :on => :create validation, utilisation after(:build) { |user| user.class.skip_callback(:validate, :create, :after, :run_something) }

12 votes

Ne serait-il pas préférable d'inverser la logique du rappel de saut ? Je veux dire, la valeur par défaut devrait être que lorsque je crée un objet, les callbacks sont déclenchés, et je devrais utiliser un paramètre différent pour le cas exceptionnel. Ainsi 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 "design", mais je pense que cela peut éviter de casser du code pré-existant, et être plus cohérent.

6 votes

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

111voto

Minimul Points 1064

Lorsque vous ne voulez pas exécuter un callback, procédez comme suit :

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

Sachez que skip_callback sera persistant dans d'autres spécifications après son exécution, envisagez 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

15 votes

J'aime mieux cette réponse parce qu'elle indique explicitement que le fait de sauter des callbacks reste au niveau de la classe, et qu'il continuera donc à sauter des callbacks dans les tests suivants.

0 votes

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

29voto

Chase T. Points 915

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

FactoryGirl.create(:user)

Exécution avec le callback after_save :

FactoryGirl.create(:user, :with_after_save_callback)

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

----------UPDATE------------ 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 du stub et du 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

0 votes

Pensez-vous que cela puisse fonctionner dans l'autre sens ? stackoverflow.com/questions/35950470/

0 votes

RuboCop se plaint de "Style/SingleLineMethods : Avoid single-line method definitions" pour la solution alternative 2, je vais donc devoir modifier le formatage, mais sinon c'est parfait !

4voto

uberllama Points 252

L'appel de skip_callback à partir de ma fabrique s'est avéré problématique pour moi.

Dans mon cas, j'ai une classe de document avec quelques callbacks liés à s3 dans before et after create que je ne veux exécuter que lorsque le test de la pile complète est nécessaire. Sinon, je veux ignorer ces callbacks s3.

Lorsque j'ai essayé skip_callbacks dans ma fabrique, il a persisté ce saut de callback même lorsque j'ai créé un objet document directement, sans utiliser de fabrique. Donc, à la place, j'ai utilisé des stubs mocha 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

0 votes

De toutes les solutions proposées ici, et pour ce qui est d'intégrer la logique dans l'usine, c'est la seule qui fonctionne avec un fichier de type before_validation crochet (en essayant de faire skip_callback avec l'un des produits FactoryGirl before o after options pour build y create n'a pas fonctionné)

3voto

Zyren Points 155

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

0 votes

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

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X