web-dev-qa-db-fra.com

Ignorer les rappels sur Factory Girl et Rspec

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
87
luizbranco

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)
104
luizbranco

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
79
Minimul

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.

27
B Seven

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
24
konyak

Un talon simple a fonctionné le mieux pour moi dans Rspec 3

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

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
5
auralbee

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
4
uberllama
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 

3
AndreiMotinga

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
3
Zyren

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) }
3
Tetiana Chupryna

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.

2
user2001785

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).

2
jake

Rails 5 - 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.

1
RudyOnRails

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