Wann RSpec let () verwenden?


447

Ich neige dazu, vor Blöcken zu verwenden, um Instanzvariablen zu setzen. Ich verwende diese Variablen dann in meinen Beispielen. Ich bin kürzlich darauf gestoßen let(). Laut RSpec-Dokumenten ist es gewohnt

... um eine gespeicherte Hilfsmethode zu definieren. Der Wert wird über mehrere Aufrufe im selben Beispiel zwischengespeichert, jedoch nicht über Beispiele hinweg.

Wie unterscheidet sich dies von der Verwendung von Instanzvariablen in Vorblöcken? Und wann sollten Sie let()vs verwenden before()?


1
Lassen Sie Blöcke träge ausgewertet werden, während Blöcke vor jedem Beispiel ausgeführt werden (sie sind insgesamt langsamer). Die Verwendung vor Blöcken hängt von Ihren persönlichen Vorlieben ab (Codierungsstil, Mocks / Stubs ...). Blöcke werden normalerweise bevorzugt. Sie können eine detailliertere Information über let
Nesha Zoric

Es wird nicht empfohlen, Instanzvariablen in einem Vor-Hook festzulegen. Schauen Sie sich betterspecs.org
Allison

Antworten:


604

Ich bevorzuge letaus mehreren Gründen immer eine Instanzvariable:

  • Instanzvariablen entstehen, wenn sie referenziert werden. Dies bedeutet, dass, wenn Sie die Schreibweise der Instanzvariablen fetten, eine neue erstellt und initialisiert wird nil, was zu subtilen Fehlern und Fehlalarmen führen kann. Da leteine Methode erstellt wird, erhalten Sie eine, NameErrorwenn Sie sie falsch schreiben, was ich vorzuziehen finde. Dies erleichtert auch die Überarbeitung von Spezifikationen.
  • before(:each)Vor jedem Beispiel wird ein Hook ausgeführt, auch wenn das Beispiel keine der im Hook definierten Instanzvariablen verwendet. Dies ist normalerweise keine große Sache, aber wenn das Einrichten der Instanzvariablen lange dauert, verschwenden Sie Zyklen. Für die durch definierte Methode wird letder Initialisierungscode nur ausgeführt, wenn das Beispiel ihn aufruft.
  • Sie können eine lokale Variable in einem Beispiel direkt in eine let umgestalten, ohne die Referenzierungssyntax im Beispiel zu ändern. Wenn Sie eine Instanzvariable umgestalten, müssen Sie ändern, wie Sie auf das Objekt im Beispiel verweisen (z @. B. ein hinzufügen ).
  • Das ist ein bisschen subjektiv, aber wie Mike Lewis betonte, macht es die Spezifikation meiner Meinung nach leichter lesbar. Ich mag die Organisation, alle meine abhängigen Objekte mit zu definieren letund meinen itBlock schön kurz zu halten.

Einen verwandten Link finden Sie hier: http://www.betterspecs.org/#let


2
Ich mag den ersten Vorteil, den Sie erwähnt haben, sehr, aber können Sie etwas mehr über den dritten erklären? Bisher verwenden die Beispiele, die ich gesehen habe (Mongoid-Spezifikationen: github.com/mongoid/mongoid/blob/master/spec/functional/mongoid/… ), einzeilige Blöcke, und ich sehe nicht, wie es ohne "@" funktioniert leichter zu lesen.
Sent-Hil

6
Wie gesagt, es ist ein bisschen subjektiv, aber ich finde es hilfreich let, alle abhängigen Objekte zu definieren und die before(:each)erforderliche Konfiguration oder die von den Beispielen benötigten Mocks / Stubs einzurichten. Ich ziehe dies einem großen Vorhaken vor, der all dies enthält. Auch let(:foo) { Foo.new }ist dann weniger laut (und mehr auf den Punkt) before(:each) { @foo = Foo.new }. Hier ist ein Beispiel, wie ich es benutze: github.com/myronmarston/vcr/blob/v1.7.0/spec/vcr/util/…
Myron Marston

Danke für das Beispiel, das hat wirklich geholfen.
Sent-Hil

3
Andrew Grimm: Stimmt, aber Warnungen können Tonnen von Lärm erzeugen (dh von Edelsteinen, die Sie verwenden und die nicht warnungsfrei sind). Außerdem NoMethodErrorbekomme ich lieber eine als eine Warnung, aber YMMV.
Myron Marston

1
@ Jwan622: Sie könnten damit beginnen, ein Beispiel zu schreiben, das foo = Foo.new(...)Benutzer und dann Benutzer fooin späteren Zeilen hat. Später schreiben Sie ein neues Beispiel in dieselbe Beispielgruppe, Foodie auf dieselbe Weise auch instanziiert werden muss. An dieser Stelle möchten Sie eine Umgestaltung vornehmen, um die Duplizierung zu beseitigen. Sie können die foo = Foo.new(...)Zeilen aus Ihren Beispielen entfernen und durch eine ersetzen, ohne let(:foo) { Foo.new(...) }die Verwendung der Beispiele zu ändern foo. Wenn Sie sich jedoch umgestalten, müssen before { @foo = Foo.new(...) }Sie auch die Referenzen in den Beispielen von foobis aktualisieren @foo.
Myron Marston

82

Der Unterschied zwischen den Instanzen Variablen und let()ist , dass let()ist faul ausgewertet . Dies bedeutet, dass dies let()erst ausgewertet wird, wenn die von ihm definierte Methode zum ersten Mal ausgeführt wird.

Der Unterschied zwischen beforeund letbesteht darin, dass let()Sie eine Gruppe von Variablen in einem kaskadierenden Stil definieren können. Auf diese Weise sieht die Spezifikation durch Vereinfachung des Codes etwas besser aus.


1
Ich verstehe, ist das wirklich ein Vorteil? Der Code wird für jedes Beispiel unabhängig davon ausgeführt.
Sent-Hil

2
Es ist einfacher, IMO zu lesen, und die Lesbarkeit ist ein großer Faktor in Programmiersprachen.
Mike Lewis

4
Senthil - es wird nicht unbedingt in jedem Beispiel ausgeführt, wenn Sie let () verwenden. Es ist faul, daher wird es nur ausgeführt, wenn darauf verwiesen wird. Im Allgemeinen spielt dies keine große Rolle, da der Sinn einer Beispielgruppe darin besteht, mehrere Beispiele in einem gemeinsamen Kontext auszuführen.
David Chelimsky

1
Heißt das, Sie sollten es nicht verwenden, letwenn Sie jedes Mal etwas bewerten müssen? Beispielsweise muss ein untergeordnetes Modell in der Datenbank vorhanden sein, bevor ein Verhalten im übergeordneten Modell ausgelöst wird. Ich verweise im Test nicht unbedingt auf dieses untergeordnete Modell, da ich das Verhalten der übergeordneten Modelle teste. Im Moment verwende ich stattdessen die let!Methode, aber vielleicht wäre es expliziter, dieses Setup einzufügen before(:each)?
Gar

2
@gar - Ich würde eine Factory (wie FactoryGirl) verwenden, mit der Sie die erforderlichen untergeordneten Assoziationen erstellen können, wenn Sie das übergeordnete Element instanziieren. Wenn Sie dies auf diese Weise tun, spielt es keine Rolle, ob Sie let () oder einen Setup-Block verwenden. Das let () ist nett, wenn Sie nicht ALLES für jeden Test in Ihren Unterkontexten verwenden müssen. Das Setup sollte nur das enthalten, was für jedes erforderlich ist.
Harmon

17

Ich habe alle Verwendungen von Instanzvariablen in meinen rspec-Tests vollständig ersetzt, um let () zu verwenden. Ich habe ein Quickie-Beispiel für einen Freund geschrieben, der damit eine kleine Rspec-Klasse unterrichtete: http://ruby-lambda.blogspot.com/2011/02/agile-rspec-with-let.html

Wie einige der anderen Antworten hier sagen, wird let () faul ausgewertet, sodass nur diejenigen geladen werden, die geladen werden müssen. Es trocknet die Spezifikation aus und macht sie lesbarer. Tatsächlich habe ich den Rspec let () -Code für die Verwendung in meinen Controllern im Stil von inherited_resource gem portiert. http://ruby-lambda.blogspot.com/2010/06/stealing-let-from-rspec.html

Neben der verzögerten Auswertung besteht der andere Vorteil darin, dass Sie in Kombination mit ActiveSupport :: Concern und der Load-Everything-In-Spezifikation / Unterstützung / Verhalten Ihre eigene Spezifikation für Ihre Anwendung erstellen können. Ich habe solche zum Testen gegen Rack- und RESTful-Ressourcen geschrieben.

Die Strategie, die ich benutze, ist Factory-Everything (über Maschinist + Fälschung / Fälscher). Es ist jedoch möglich, es in Kombination mit vor (: jedem) Blöcken zu verwenden, um Fabriken für eine ganze Reihe von Beispielgruppen vorzuladen, sodass die Spezifikationen schneller ausgeführt werden können: http://makandra.com/notes/770-taking-advantage -of-rspec-s-eingelassen-vor-Blöcken


Hey Ho-Sheng, ich habe tatsächlich einige Ihrer Blog-Beiträge gelesen, bevor ich diese Frage gestellt habe. Glauben Sie nicht, dass sie # spec/friendship_spec.rbund Ihr # spec/comment_spec.rbBeispiel weniger lesbar sind? Ich habe keine Ahnung, woher usersich komme und muss tiefer graben.
Sent-Hil

Das erste Dutzend Leute, denen ich das Format gezeigt habe, finden es viel besser lesbar, und einige von ihnen haben angefangen, damit zu schreiben. Ich habe jetzt genug Spezifikationscode mit let (), so dass ich auch auf einige dieser Probleme stoße. Ich gehe zum Beispiel und arbeite mich ausgehend von der innersten Beispielgruppe wieder hoch. Dies ist die gleiche Fähigkeit wie die Verwendung einer hoch meta-programmierbaren Umgebung.
Ho-Sheng Hsiao

2
Das größte Problem, auf das ich gestoßen bin, ist die versehentliche Verwendung von let (: subject) {} anstelle von subject {}. subject () ist anders eingerichtet als let (: subject), aber let (: subject) überschreibt es.
Ho-Sheng Hsiao

1
Wenn Sie das Drilldown in den Code loslassen können, werden Sie feststellen, dass das Scannen eines Codes mit let () -Deklarationen viel, viel schneller erfolgt. Es ist einfacher, beim Scannen von Code let () -Deklarationen auszuwählen, als im Code eingebettete @ -Variablen zu finden. Mit @variables habe ich keine gute "Form", für welche Zeilen sich auf die Zuordnung zu den Variablen beziehen und welche Zeilen sich auf das Testen der Variablen beziehen. Mit let () werden alle Zuweisungen mit let () ausgeführt, sodass Sie "sofort" anhand der Form der Buchstaben wissen, in denen sich Ihre Deklarationen befinden.
Ho-Sheng Hsiao

1
Sie können das gleiche Argument dafür vorbringen, dass Instanzvariablen leichter herauszusuchen sind, insbesondere da einige Editoren wie meine (gedit) Instanzvariablen hervorheben. Ich habe let()die letzten paar Tage benutzt und persönlich sehe ich keinen Unterschied, außer dem ersten Vorteil, den Myron erwähnte. Und ich bin mir nicht so sicher, ob ich loslassen soll und was nicht, vielleicht weil ich faul bin und Code gerne im Voraus sehe, ohne noch eine Datei öffnen zu müssen. Danke für deine Kommentare.
-Hil

13

Es ist wichtig zu beachten, dass let faul ausgewertet wird und keine Nebenwirkungsmethoden verwendet werden, da Sie sonst nicht einfach von let zu before (: each) wechseln können. Sie können let verwenden! anstatt zu lassen, damit es vor jedem Szenario ausgewertet wird.


8

Im Allgemeinen let()handelt es sich um eine schönere Syntax, mit der Sie @nameüberall Symbole eingeben können . Aber Vorbehalt Emptor! Ich habe festgestellt, dass es let()auch subtile Fehler (oder zumindest Kopfkratzer) gibt, da die Variable erst dann wirklich existiert, wenn Sie versuchen, sie zu verwenden ... Tell Tale-Zeichen: Wenn Sie ein putsnach dem hinzufügen, um let()zu sehen, ob die Variable korrekt ist, wird eine Spezifikation zugelassen zu bestehen, aber ohne dass die putsSpezifikation fehlschlägt - Sie haben diese Subtilität gefunden.

Ich habe auch festgestellt, dass let()das nicht unter allen Umständen zwischenzuspeichern scheint! Ich habe es in meinem Blog geschrieben: http://technicaldebt.com/?p=1242

Vielleicht bin es nur ich?


9
letspeichert immer den Wert für die Dauer eines einzelnen Beispiels. Der Wert wird nicht über mehrere Beispiele hinweg gespeichert. before(:all)Im Gegensatz dazu können Sie eine initialisierte Variable in mehreren Beispielen wiederverwenden.
Myron Marston

2
Wenn Sie let verwenden möchten (wie es jetzt als Best Practice zu gelten scheint), aber eine bestimmte Variable benötigen, die sofort instanziiert werden soll, let!ist dies das Richtige für Sie. relishapp.com/rspec/rspec-core/v/2-6/docs/helper-methods/…
Jacob

6

let ist funktional, da es im Wesentlichen ein Proc ist. Auch es ist zwischengespeichert.

Ein Gotcha, das ich sofort mit let gefunden habe ... In einem Spec-Block, der eine Änderung auswertet.

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

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

Sie müssen sicher sein, dass Sie letaußerhalb Ihres Erwartungsblocks anrufen . dh Sie rufen FactoryGirl.createin Ihrem Let-Block an. Normalerweise überprüfe ich, ob das Objekt erhalten bleibt.

object.persisted?.should eq true

Andernfalls letwird beim ersten Aufruf des Blocks aufgrund der verzögerten Instanziierung tatsächlich eine Änderung in der Datenbank vorgenommen.

Aktualisieren

Einfach eine Notiz hinzufügen. Seien Sie vorsichtig beim Spielen von Code Golf oder in diesem Fall Rspec Golf mit dieser Antwort.

In diesem Fall muss ich nur eine Methode aufrufen, auf die das Objekt reagiert. Also rufe ich die _.persisted?_-Methode für das Objekt als seine Wahrheit auf. Ich versuche nur, das Objekt zu instanziieren. Sie könnten leer anrufen? oder null? zu. Es geht nicht um den Test, sondern darum, das Objekt zum Leben zu erwecken, indem man es nennt.

Sie können also nicht umgestalten

object.persisted?.should eq true

sein

object.should be_persisted 

da das Objekt nicht instanziiert wurde ... ist es faul. :) :)

Update 2

Nutzen Sie die Vermietung! Syntax für die sofortige Objekterstellung, die dieses Problem insgesamt vermeiden sollte. Beachten Sie jedoch, dass dies einen Großteil des Zwecks der Faulheit des nicht geknallten Let zunichte macht.

In einigen Fällen möchten Sie möglicherweise auch die Betreff-Syntax nutzen, anstatt sie zuzulassen, da Sie dadurch möglicherweise zusätzliche Optionen erhalten.

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

2

Hinweis für Joseph: Wenn Sie Datenbankobjekte in einer Datenbank erstellen, werden before(:all)diese nicht in einer Transaktion erfasst, und es ist viel wahrscheinlicher, dass Sie Cruft in Ihrer Testdatenbank belassen. Verwenden Sie before(:each)stattdessen.

Der andere Grund für die Verwendung von let und seiner verzögerten Auswertung besteht darin, dass Sie ein kompliziertes Objekt nehmen und einzelne Teile testen können, indem Sie let-in-Kontexte überschreiben, wie in diesem sehr konstruierten Beispiel:

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 bevor (: alle) Fehler viele Tage unserer Entwicklerzeit verschwendet haben.
Michael Durrant

1

"vor" impliziert standardmäßig before(:each). Siehe The Rspec Book, Copyright 2010, Seite 228.

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

Ich verwende, before(:each)um einige Daten für jede Beispielgruppe zu setzen, ohne die letMethode aufrufen zu müssen, um die Daten im "it" -Block zu erstellen. In diesem Fall weniger Code im "it" -Block.

Ich verwende, letwenn ich einige Daten in einigen Beispielen möchte, andere jedoch nicht.

Sowohl vorher als auch lassen sind großartig, um die "es" -Blöcke aufzutrocknen.

Um Verwirrung zu vermeiden, ist "let" nicht dasselbe wie before(:all). "Let" bewertet seine Methode und seinen Wert für jedes Beispiel neu ("it"), speichert den Wert jedoch zwischen mehreren Aufrufen im selben Beispiel zwischen. Weitere Informationen finden Sie hier: https://www.relishapp.com/rspec/rspec-core/v/2-6/docs/helper-methods/let-and-let


1

Gegenstimme hier: Nach 5 Jahren rspec mag ich nicht letsehr.

1. Eine träge Auswertung macht den Testaufbau oft verwirrend

Es wird schwierig, über das Setup nachzudenken, wenn einige Dinge, die im Setup deklariert wurden, den Status nicht tatsächlich beeinflussen, während andere dies tun.

Irgendwann wechselt jemand aus Frustration einfach letzu let!(dasselbe ohne faule Bewertung), um seine Spezifikation zum Laufen zu bringen. Wenn dies für sich arbeitet, wird eine neue Gewohnheit geboren: wenn eine neue Spezifikation zu einer älteren Suite hinzugefügt wird und es nicht funktioniert, die erste Sache , versucht der Schriftsteller Pony zufällig hinzufügen letAnrufe.

Ziemlich bald sind alle Leistungsvorteile weg.

2. Spezielle Syntax ist für Nicht-Rspec-Benutzer ungewöhnlich

Ich würde meinem Team lieber Ruby beibringen als die Tricks von rspec. Instanzvariablen oder Methodenaufrufe sind überall in diesem und anderen Projekten letnützlich. Die Syntax ist nur in rspec nützlich.

3. Die "Vorteile" ermöglichen es uns, gute Designänderungen leicht zu ignorieren

let()ist gut für teure Abhängigkeiten, die wir nicht immer wieder erstellen möchten. Es passt auch gut zu subject, sodass Sie wiederholte Aufrufe von Methoden mit mehreren Argumenten austrocknen können

Teure Abhängigkeiten, die oft wiederholt werden, und Methoden mit großen Signaturen sind beide Punkte, an denen wir den Code verbessern könnten:

  • Vielleicht kann ich eine neue Abstraktion einführen, die eine Abhängigkeit vom Rest meines Codes isoliert (was bedeuten würde, dass weniger Tests sie benötigen).
  • Vielleicht macht der getestete Code zu viel
  • Vielleicht muss ich intelligentere Objekte anstelle einer langen Liste von Grundelementen injizieren
  • Vielleicht habe ich eine Verletzung von Tell-Don't-Ask
  • Vielleicht kann der teure Code schneller gemacht werden (seltener - Vorsicht vor vorzeitiger Optimierung hier)

In all diesen Fällen kann ich das Symptom schwieriger Tests mit einem beruhigenden Balsam aus Rspec-Magie behandeln oder versuchen, die Ursache zu beheben. Ich habe das Gefühl, dass ich in den letzten Jahren viel zu viel mit dem ersteren verbracht habe und jetzt möchte ich einen besseren Code.

Um die ursprüngliche Frage zu beantworten: Ich würde es lieber nicht tun, aber ich benutze es immer noch let. Ich benutze es meistens, um mich dem Stil des restlichen Teams anzupassen (es scheint, als ob die meisten Rails-Programmierer auf der Welt jetzt tief in ihrer Rspec-Magie stecken, so dass dies sehr oft der Fall ist). Manchmal verwende ich es, wenn ich einem Code, über den ich keine Kontrolle habe, einen Test hinzufüge oder keine Zeit habe, eine bessere Abstraktion vorzunehmen: dh wenn die einzige Option das Schmerzmittel ist.


0

Ich letteste meine HTTP 404-Antworten in meinen API-Spezifikationen mithilfe von Kontexten.

Um die Ressource zu erstellen, verwende ich let!. Aber um die Ressourcenkennung zu speichern, verwende ich let. Schauen Sie sich an, wie es aussieht:

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

Das hält die Spezifikationen sauber und lesbar.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.