Quand utiliser RSpec let ()?


447

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

... pour définir une méthode d'aide mémorisée. La valeur sera mise en cache sur plusieurs appels dans le même exemple, mais pas sur plusieurs exemples.

En quoi est-ce différent de l'utilisation de variables d'instance dans des blocs avant? Et aussi quand faut-il utiliser let()vs before()?


1
Soit les blocs sont évalués paresseusement, tandis qu'avant l'exécution des blocs avant chaque exemple (ils sont globalement plus lents). L'utilisation de blocs avant dépend de vos préférences personnelles (style de codage, mocks / stubs ...). Laissez les blocs sont généralement préférés. Vous pouvez consulter une information plus détaillée sur let
Nesha Zoric

Il n'est pas recommandé de définir des variables d'instance dans un hook avant. Découvrez betterspecs.org
Allison

Réponses:


604

Je préfère toujours letune variable d'instance pour deux raisons:

  • Les variables d'instance surgissent lorsqu'elles sont référencées. Cela signifie que si vous touchez l'orthographe de la variable d'instance, une nouvelle sera créée et initialisée nil, ce qui peut entraîner des bogues subtils et des faux positifs. Depuis letcrée une méthode, vous obtiendrez un NameErrorlorsque vous l'orthographiez mal, ce que je trouve préférable. Cela facilite également la refonte des spécifications.
  • Un before(:each)hook sera exécuté 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 du temps, 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 un let sans changer la syntaxe de référencement dans l'exemple. Si vous refactorisez une variable d'instance, vous devez changer la façon dont vous référencez l'objet dans l'exemple (par exemple, ajoutez 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 objets dépendants avec letet de garder mon itbloc agréable et court.

Un lien connexe peut être trouvé ici: http://www.betterspecs.org/#let


2
J'aime vraiment le premier avantage que vous avez mentionné, mais pourriez-vous expliquer un peu plus le troisième? Jusqu'à présent, les exemples que j'ai vus (spécifications mongoid: github.com/mongoid/mongoid/blob/master/spec/functional/mongoid/… ) utilisent des blocs de ligne unique et je ne vois pas comment le fait de ne pas avoir "@" en fait plus facile à lire.
sent-hil

6
Comme je l'ai dit, c'est un peu subjectif, mais je trouve utile de l'utiliser letpour définir tous les objets dépendants, et de l'utiliser before(:each)pour configurer la configuration requise ou tout faux / stubs requis par les exemples. Je préfère cela à un grand crochet avant contenant tout cela. Aussi, let(:foo) { Foo.new }c'est moins bruyant (et plus pertinent) alors before(:each) { @foo = Foo.new }. Voici un exemple de la façon dont je l'utilise: github.com/myronmarston/vcr/blob/v1.7.0/spec/vcr/util/…
Myron Marston

Merci pour l'exemple, cela a vraiment aidé.
sent-hil

3
Andrew Grimm: vrai, mais les avertissements peuvent générer des tonnes de bruit (c'est-à-dire des gemmes que vous utilisez qui ne fonctionnent pas sans avertissement). De plus, je préfère obtenir NoMethodErrorun avertissement, mais YMMV.
Myron Marston

1
@ Jwan622: vous pourriez commencer par écrire un exemple, qui a foo = Foo.new(...)et ensuite des utilisateurs foosur des lignes ultérieures. Plus tard, vous écrivez un nouvel exemple dans le même groupe d'exemples qui nécessite également une Fooinstanciation de la même manière. À ce stade, vous souhaitez refactoriser pour éliminer la duplication. Vous pouvez supprimer les foo = Foo.new(...)lignes de vos exemples et les remplacer par un let(:foo) { Foo.new(...) }sans changer la façon dont les exemples sont utilisés foo. Mais si vous refactorisez, before { @foo = Foo.new(...) }vous devez également mettre à jour les références dans les exemples de fooà @foo.
Myron Marston

82

La différence entre l'utilisation de variables d'instance et let()celle qui let()est évaluée paresseusement . 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 beforeet letest que cela let()vous donne une belle façon 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.


1
Je vois, est-ce vraiment un avantage? Quoi qu'il en soit, le code est exécuté pour chaque exemple.
sent-hil

2
Il est plus facile de lire IMO, et la lisibilité est un facteur énorme dans les langages de programmation.
Mike Lewis

4
Senthil - il n'est en fait pas nécessairement exécuté dans tous les exemples lorsque vous utilisez let (). Il est paresseux, il n'est donc exécuté que s'il est référencé. De manière générale, cela n'a pas beaucoup d'importance car le but d'un groupe d'exemples est de faire exécuter plusieurs exemples dans un contexte commun.
David Chelimsky

1
Cela signifie-t-il que vous ne devriez pas l'utiliser letsi vous avez besoin que quelque chose soit évalué à chaque fois? Par exemple, j'ai besoin qu'un modèle enfant soit présent dans la base de données avant qu'un comportement ne se déclenche sur le modèle parent. Je ne fais pas nécessairement référence à ce modèle enfant dans le test, car je teste le comportement des modèles parents. Pour le moment, j'utilise la let!méthode à la place, mais peut-être qu'il serait plus explicite de mettre cette configuration before(:each)?
gar

2
@gar - J'utiliserais une Factory (comme FactoryGirl) qui vous permet de créer les associations enfants requises lorsque vous instanciez le parent. Si vous le faites de cette façon, cela n'a pas vraiment d'importance si vous utilisez let () ou un bloc d'installation. Le let () est bien si vous n'avez pas besoin d'utiliser TOUT pour chaque test dans vos sous-contextes. Le programme d'installation doit avoir uniquement ce qui est requis pour chacun.
Harmon

17

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

Comme le disent certaines des autres réponses ici, let () est évalué paresseux donc il ne chargera 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 gem hérité_ressource. http://ruby-lambda.blogspot.com/2010/06/stealing-let-from-rspec.html

Avec l'évaluation paresseuse, l'autre avantage est que, combiné à ActiveSupport :: Concern, et à la spécification / support / comportement tout-en-un, vous pouvez créer votre propre mini-DSL de spécification spécifique à votre application. J'ai écrit des tests pour 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 (: chaque) pour précharger les usines pour un ensemble complet de groupes d'exemples, 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


Hé Ho-Sheng, j'ai lu plusieurs de vos articles de blog avant de poser cette question. En ce qui concerne votre # spec/friendship_spec.rbet # spec/comment_spec.rbexemple, ne pensez-vous pas qu'ils le rendent moins lisible? Je n'ai aucune idée d'où usersviennent et devront creuser plus profondément.
sent-hil

La première douzaine de personnes dont j'ai montré le format le trouvent toutes beaucoup plus lisible, et quelques-unes d'entre elles ont commencé à l'écrire. J'ai maintenant suffisamment de code de spécification en utilisant let () pour que je rencontre également certains de ces problèmes. Je me retrouve à aller à l'exemple, et à partir du groupe d'exemples le plus intérieur, je me remets en marche. C'est la même compétence que l'utilisation d'un environnement hautement méta-programmable.
Ho-Sheng Hsiao

2
Le plus gros problème que j'ai rencontré est d'utiliser accidentellement let (: subject) {} au lieu de subject {}. subject () est configuré différemment de let (: subject), mais let (: subject) le remplacera.
Ho-Sheng Hsiao

1
Si vous pouvez laisser aller "en avant" dans le code, alors vous trouverez l'analyse d'un code avec les déclarations let () beaucoup, beaucoup plus rapidement. Il est plus facile de sélectionner des déclarations let () lors de la numérisation de code que de trouver des variables @ incorporées dans le code. En utilisant @variables, je n'ai pas une bonne "forme" pour quelles lignes se réfèrent à l'affectation aux variables et quelles lignes se réfèrent au test des variables. En utilisant let (), toutes les affectations sont effectuées avec let () afin que vous sachiez "instantanément" par la forme des lettres où se trouvent vos déclarations.
Ho-Sheng Hsiao

1
Vous pouvez rendre le même argument selon lequel les variables d'instance sont plus faciles à sélectionner, d'autant plus que certains éditeurs, comme le mien (gedit) mettent en évidence les variables d'instance. J'utilise let()les deux derniers jours et personnellement, je ne vois pas de différence, à l'exception du premier avantage mentionné par Myron. Et je ne suis pas sûr de lâcher prise et de ne pas le faire, peut-être parce que je suis paresseux et j'aime voir le code à l'avance sans avoir à ouvrir encore un autre fichier. Merci pour vos commentaires.
sent-hil

13

Il est important de garder à l'esprit que let est évalué paresseux et ne pas y mettre de méthodes d'effets secondaires sinon vous ne pourriez pas passer facilement de let à avant (: chaque) . Vous pouvez utiliser let! au lieu de laisser afin qu'il soit évalué avant chaque scénario.


8

En général, let()c'est une syntaxe plus agréable, et cela vous évite de taper des @namesymboles partout. Mais, caveat emptor! J'ai trouvé let()aussi introduit des bugs subtils (ou au moins gratter la tête) , car la variable n'existe pas vraiment jusqu'à ce que vous essayez de l' utiliser ... Dites signe de conte: si l' ajout d' un putsaprès let()pour voir que la variable est correcte permet une spécification pour passer, mais sans la putsspécification échoue - vous avez trouvé cette subtilité.

J'ai également constaté que let()cela ne semble pas en cache en toutes circonstances! Je l'ai écrit dans mon blog: http://technicaldebt.com/?p=1242

C'est peut-être juste moi?


9
letmémorise toujours la valeur pour la durée d'un seul exemple. Il ne mémorise pas la valeur dans plusieurs exemples. before(:all), en revanche, vous permet de réutiliser une variable initialisée dans plusieurs exemples.
Myron Marston

2
si vous voulez utiliser let (comme cela semble maintenant être considéré comme la meilleure pratique), mais que vous avez besoin d'une variable particulière pour être instanciée immédiatement, c'est let!pour cela. relishapp.com/rspec/rspec-core/v/2-6/docs/helper-methods/…
Jacob

6

Let est fonctionnel car c'est essentiellement un Proc. Aussi son caché.

Un problème 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 être sûr d'appeler en letdehors de votre bloc d'attente. c'est-à-dire que vous appelez FactoryGirl.createdans votre bloc let. Je le fais généralement en vérifiant que l'objet est persistant.

object.persisted?.should eq true

Sinon, lorsque le letbloc est appelé la première fois, un changement dans la base de données se produit en raison de l'instanciation paresseuse.

Mise à jour

Il suffit d'ajouter une note. Soyez prudent en jouant au golf à code ou dans ce cas au golf rspec avec cette réponse.

Dans ce cas, je n'ai qu'à appeler une méthode à laquelle l'objet répond. _.persisted?J'invoque donc la méthode _ sur l'objet comme étant véridique. Tout ce que j'essaie de faire, c'est d'instancier l'objet. Vous pourriez appeler vide? ou nul? aussi. Le point n'est pas le test mais donner vie à l'objet en l'appelant.

Vous ne pouvez donc pas refactoriser

object.persisted?.should eq true

être

object.should be_persisted 

comme l'objet n'a pas été instancié ... c'est paresseux. :)

Update 2

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

De plus, dans certains cas, vous souhaiterez peut-être utiliser la syntaxe du sujet au lieu de la laisser car cela peut vous donner des options supplémentaires.

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

2

Remarque à Joseph - si vous créez des objets de base de données dans un, before(:all)ils ne seront pas capturés dans une transaction et vous êtes beaucoup plus susceptible de laisser des fichiers dans votre base de données de test. Utilisez before(:each)plutôt.

L'autre raison d'utiliser let et son évaluation paresseuse est que vous puissiez prendre un objet 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

1
+1 avant (: tous) les bogues ont gaspillé plusieurs jours de temps de nos développeurs.
Michael Durrant

1

"avant" par défaut implique before(:each). Réf The Rspec Book, copyright 2010, page 228.

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

J'utilise before(:each)pour amorcer des données pour chaque groupe d'exemple sans avoir à appeler la letméthode pour créer les données dans le bloc "it". Moins de code dans le bloc "it" dans ce cas.

J'utilise letsi je veux des données dans certains exemples mais pas dans d'autres.

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

Pour éviter toute confusion, "laisser" n'est pas la même chose que 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


1

Voix dissidente ici: après 5 ans de rspec je n'aime pas lettrop.

1. L'évaluation paresseuse rend souvent la configuration du test confuse

Il devient difficile de raisonner sur la configuration lorsque certaines choses qui ont été déclarées dans la configuration n'affectent pas réellement l'état, tandis que d'autres le sont.

Finalement, de quelqu'un de frustration change juste letà let!(même chose sans évaluation paresseuse) afin d'obtenir leur travail spec. Si cela fonctionne pour eux, une nouvelle habitude est née: lorsqu'une nouvelle spécification est ajoutée à une suite plus ancienne et que cela ne fonctionne pas, la première chose que l'écrivain essaie est d'ajouter une frange aux letappels aléatoires .

Bientôt, tous les avantages de performance ont disparu.

2. La syntaxe spéciale est inhabituelle pour les utilisateurs non-rspec

Je préfère enseigner Ruby à mon équipe que les astuces de rspec. Les variables d'instance ou les appels de méthode sont utiles partout dans ce projet et dans d'autres, la letsyntaxe ne sera utile que dans rspec.

3. Les "avantages" nous permettent d'ignorer facilement les bonnes modifications de conception

let()est bon pour les dépendances coûteuses que nous ne voulons pas créer encore et encore. Il s'associe également bien avec subject, vous permettant de sécher les appels répétés aux méthodes multi-arguments

Des dépendances coûteuses répétées plusieurs fois et des méthodes avec de grandes signatures sont deux points où nous pourrions améliorer le code:

  • je peux peut-être introduire une nouvelle abstraction qui isole une dépendance du reste de mon code (ce qui signifierait que moins de tests en ont besoin)
  • peut-être que le code testé en fait trop
  • peut-être que je dois injecter des objets plus intelligents au lieu d'une longue liste de primitives
  • peut-être que j'ai une violation de ne pas demander
  • peut-être que le code coûteux peut être rendu plus rapide (plus rare - méfiez-vous d'une optimisation prématurée ici)

Dans tous ces cas, je peux traiter le symptôme de tests difficiles avec un baume apaisant de magie rspec, ou je peux essayer de traiter la cause. J'ai l'impression d'avoir passé beaucoup trop de temps ces dernières années sur le premier et maintenant je veux un meilleur code.

Pour répondre à la question d'origine: je préférerais ne pas le faire, mais j'utilise toujours let. Je l' utilise principalement pour m'adapter au style du reste de l'équipe (il semble que la plupart des programmeurs Rails dans le monde soient maintenant profondément plongés dans leur magie rspec, ce qui est très souvent). Parfois, je l'utilise lorsque j'ajoute un test à un code que je n'ai pas le contrôle ou que je n'ai pas le temps de refactoriser pour une meilleure abstraction: c'est-à-dire lorsque la seule option est l'analgésique.


0

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 ressource, j'utilise let. Regardez à quoi ça ressemble:

let!(:country)   { create(:country) }
let(:country_id) { country.id }
before           { get "api/countries/#{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 maintient les spécifications propres et lisibles.

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.