Quand utiliser RSpec let ()?

J’ai tendance à utiliser avant les blocs pour définir des variables d’instance. J’utilise ensuite ces variables dans mes exemples. Je suis récemment tombé sur let() . Selon RSpec docs, il est habitué à

… pour définir une méthode d’aide mémo. La valeur sera mise en cache sur plusieurs appels dans le même exemple, mais pas entre les exemples.

En quoi est-ce différent d’utiliser des variables d’instance avant les blocs? Et aussi quand faut-il utiliser let() vs before() ?

Je préfère toujours let une variable d’instance pour deux raisons:

  • Les variables d’instance apparaissent dès qu’elles sont référencées. Cela signifie que si vous écrasez l’orthographe de la variable d’instance, une nouvelle sera créée et initialisée à nil , ce qui peut entraîner des bogues et des faux positifs subtils. Puisque let crée une méthode, vous obtiendrez une NameError lorsque vous l’orthographiez mal, ce que je trouve préférable. Cela facilite aussi la refactorisation des spécifications.
  • Un hook before(:each) s’exécutera avant chaque exemple, même si l’exemple n’utilise aucune des variables d’instance définies dans le hook. Ce n’est généralement pas un gros problème, mais si la configuration de la variable d’instance prend beaucoup de temps, alors vous perdez des cycles. Pour la méthode définie par let , le code d’initialisation ne s’exécute que si l’exemple l’appelle.
  • Vous pouvez refactoriser une variable locale dans un exemple directement dans une let sans modifier la syntaxe de référence dans l’exemple. Si vous refactorez une variable d’instance, vous devez modifier la manière dont vous référencez l’object dans l’exemple (par exemple, append un @ ).
  • C’est un peu subjectif, mais comme Mike Lewis l’a souligné, je pense que cela rend la spécification plus facile à lire. J’aime l’organisation de définir tous mes objects dépendants avec let et de garder mon bloc court et agréable.

La différence entre l’utilisation de variables d’instance et let() est que let() est évalué paresseux . Cela signifie que let() n’est pas évalué tant que la méthode qu’il définit n’est pas exécutée pour la première fois.

La différence entre before et let est que let() vous permet de définir un groupe de variables dans un style en cascade. Ce faisant, la spécification est un peu meilleure en simplifiant le code.

J’ai complètement remplacé toutes les utilisations de variables d’instance dans mes tests rspec pour utiliser let (). J’ai écrit un exemple rapide pour un ami qui l’utilisait pour enseigner une petite classe Rspec: http://ruby-lambda.blogspot.com/2011/02/agile-rspec-with-let.html

Comme certaines autres réponses l’indiquent ici, let () est évalué paresseux pour ne charger que celles qui nécessitent un chargement. Il sèche la spécification et la rend plus lisible. J’ai en fait porté le code Rspec let () à utiliser dans mes contrôleurs, dans le style de la gemme inherited_resource. http://ruby-lambda.blogspot.com/2010/06/stealing-let-from-rspec.html

Parallèlement à l’évaluation paresseuse, l’autre avantage est que, associé à ActiveSupport :: Concern et à la spécification / support / comportement load-everything-in, vous pouvez créer votre propre mini-DSL de spécifications spécifique à votre application. J’en ai écrit d’autres pour tester les ressources Rack et RESTful.

La stratégie que j’utilise est Factory-Everything (via Machinist + Forgery / Faker). Cependant, il est possible de l’utiliser en combinaison avec des blocs avant (: chacun) pour précharger des fabriques pour un ensemble complet de groupes d’exemple, ce qui permet aux spécifications de s’exécuter plus rapidement: http://makandra.com/notes/770-taking-advantage -of-rspec-s-let-in-before-blocks

Il est important de garder à l’esprit que let est évalué paresseux et ne contient pas de méthode d’effet secondaire, sinon vous ne pourrez pas passer facilement de let à before (: chacun) . Vous pouvez utiliser let! au lieu de laisser pour qu’il soit évalué avant chaque scénario.

En général, let() est une syntaxe plus agréable et vous évite de taper des symboles @name partout. Mais, caveat emptor! J’ai trouvé let() qui introduit aussi des bogues subtils (ou au moins un grattement de tête) car la variable n’existe pas tant que vous n’avez pas essayé de l’utiliser … la variable est correcte permet à une spécification de passer, mais sans les puts la spécification échoue – vous avez trouvé cette subtilité.

J’ai aussi trouvé que let() ne semble pas se cacher en toutes circonstances! Je l’ai écrit sur mon blog: http://technicaldebt.com/?p=1242

Peut-être que c’est juste moi?

let est fonctionnel car c’est essentiellement un Proc. Aussi son cache.

Un piège que j’ai trouvé tout de suite avec let … Dans un bloc Spec qui évalue un changement.

 let(:object) {FactoryGirl.create :object} expect { post :destroy, id: review.id }.to change(Object, :count).by(-1) 

Vous devez vous assurer d’appeler let dehors de votre attente. c’est-à-dire que vous appelez FactoryGirl.create dans votre bloc let. Je le fais généralement en vérifiant que l’object est persisté.

 object.persisted?.should eq true 

Sinon, lorsque le bloc let est appelé la première fois, une modification de la firebase database se produira en raison de l’instanciation paresseuse.

Mettre à jour

Juste en ajoutant une note. Soyez prudent en jouant au golf de code ou dans ce cas, rspec golf avec cette réponse.

Dans ce cas, je dois juste appeler une méthode à laquelle l’object répond. Alors j’invoque le _.persisted? _ méthode sur l’object comme sa vérité. Tout ce que j’essaie de faire est d’instancier l’object. Vous pourriez appeler vide? ou nul? aussi. Le point n’est pas le test, mais en apportant l’object de la vie en l’appelant.

Donc, vous ne pouvez pas refactoriser

 object.persisted?.should eq true 

être

 object.should be_persisted 

comme l’object n’a pas été instancié … c’est paresseux. 🙂

Mise à jour 2

tirer parti du let! syntaxe pour la création instantanée d’objects, ce qui devrait éviter complètement ce problème. Notez bien que cela ira à l’encontre de la paresse de la non claquée.

De même, dans certains cas, vous pourriez vouloir tirer parti de la syntaxe du sujet au lieu de laisser car cela peut vous donner des options supplémentaires.

 subject(:object) {FactoryGirl.create :object} 

Note à Joseph – si vous créez des objects de firebase database dans un before(:all) ils ne seront pas capturés dans une transaction et vous risquez fort de laisser des objects inutiles dans votre firebase database de test. Utilisez before(:each) place.

L’autre raison d’utiliser let et son évaluation paresseuse est que vous pouvez prendre un object compliqué et tester des pièces individuelles en remplaçant let dans des contextes, comme dans cet exemple très artificiel:

 context "foo" do let(:params) do { :foo => foo, :bar => "bar" } end let(:foo) { "foo" } it "is set to foo" do params[:foo].should eq("foo") end context "when foo is bar" do let(:foo) { "bar" } # NOTE we didn't have to redefine params entirely! it "is set to bar" do params[:foo].should eq("bar") end end end 

“avant” par défaut implique before(:each) . Ref Le livre Rspec, copyright 2010, page 228.

 before(scope = :each, options={}, &block) 

J’utilise before(:each) pour générer des données pour chaque groupe d’exemple sans avoir à appeler la méthode let pour créer les données dans le bloc “it”. Moins de code dans le bloc “it” dans ce cas.

J’utilise let si je veux des données dans certains exemples mais pas d’autres.

Les deux avant et laisser sont parfaits pour sécher les blocs “it”.

Pour éviter toute confusion, “let” n’est pas la même chose before(:all) . “Let” réévalue sa méthode et sa valeur pour chaque exemple (“it”), mais met en cache la valeur sur plusieurs appels dans le même exemple. Vous pouvez en savoir plus à ce sujet ici: https://www.relishapp.com/rspec/rspec-core/v/2-6/docs/helper-methods/let-and-let

J’utilise let pour tester mes réponses HTTP 404 dans mes spécifications API en utilisant des contextes.

Pour créer la ressource, j’utilise let! . Mais pour stocker l’identifiant de la ressource, j’utilise let . Jetez un coup d’oeil à quoi ça ressemble:

 let!(:country) { create(:country) } let(:country_id) { country.id } before { get "api/counsortinges/#{country_id}" } it 'responds with HTTP 200' { should respond_with(200) } context 'when the country does not exist' do let(:country_id) { -1 } it 'responds with HTTP 404' { should respond_with(404) } end 

Cela permet de garder les spécifications propres et lisibles.