Quando utilizzare RSpec let ()?


447

Tendo a usare prima dei blocchi per impostare le variabili di istanza. Quindi uso quelle variabili nei miei esempi. Di recente mi sono imbattuto let(). Secondo i documenti RSpec, è abituato

... per definire un metodo di supporto memorizzato. Il valore verrà memorizzato nella cache su più chiamate nello stesso esempio ma non tra esempi.

In che modo differisce dall'uso delle variabili di istanza nei blocchi precedenti? E anche quando dovresti usare let()vs before()?


1
Lascia che i blocchi vengano valutati pigramente, mentre prima che i blocchi vengano eseguiti prima di ogni esempio (sono complessivamente più lenti). L'uso prima dei blocchi dipende dalle preferenze personali (stile di codifica, mock / stub ...). Di solito sono preferiti i blocchi. È possibile controllare informazioni
Nesha Zoric

Non è buona norma impostare variabili di istanza in un hook precedente. Dai
Allison il

Risposte:


604

Preferisco sempre letuna variabile di istanza per un paio di motivi:

  • Le variabili di istanza nascono quando vengono referenziate. Ciò significa che se si digita con precisione l'ortografia della variabile di istanza, ne verrà creata e inizializzata una nuova nil, che può portare a bug sottili e falsi positivi. Dal momento che letcrea un metodo, otterrai un NameErrorerrore ortografico, che trovo preferibile. Semplifica anche il refactoring delle specifiche.
  • Un before(:each)hook verrà eseguito prima di ogni esempio, anche se l'esempio non utilizza nessuna delle variabili di istanza definite nel hook. Questo di solito non è un grosso problema, ma se l'installazione della variabile di istanza richiede molto tempo, allora stai sprecando cicli. Per il metodo definito da let, il codice di inizializzazione viene eseguito solo se l'esempio lo chiama.
  • È possibile eseguire il refactoring da una variabile locale in un esempio direttamente in un let senza modificare la sintassi di riferimento nell'esempio. Se si esegue il refactoring a una variabile di istanza, è necessario modificare il modo in cui si fa riferimento all'oggetto nell'esempio (ad es. Aggiungere un@ ).
  • Questo è un po 'soggettivo, ma come ha sottolineato Mike Lewis, penso che renda le specifiche più facili da leggere. Mi piace l'organizzazione di definire tutti i miei oggetti dipendenti con lete mantenere il mio itblocco bello e breve.

Un collegamento correlato è disponibile qui: http://www.betterspecs.org/#let


2
Mi piace molto il primo vantaggio che hai citato, ma potresti spiegare un po 'di più sul terzo? Finora gli esempi che ho visto (specifiche mongoid: github.com/mongoid/mongoid/blob/master/spec/functional/mongoid/… ) usano blocchi a linea singola e non vedo come non avere "@" lo rende più facile da leggere.
inviato il

6
Come ho detto, è un po 'soggettivo, ma trovo utile usare letper definire tutti gli oggetti dipendenti e usare before(:each)per impostare la configurazione necessaria o eventuali mock / stub necessari negli esempi. Preferisco questo a un gancio grande prima contenente tutto questo. Inoltre, let(:foo) { Foo.new }è meno rumoroso (e più al punto) quindi before(:each) { @foo = Foo.new }. Ecco un esempio di come lo uso: github.com/myronmarston/vcr/blob/v1.7.0/spec/vcr/util/…
Myron Marston

Grazie per l'esempio, ciò mi ha davvero aiutato.
inviato il

3
Andrew Grimm: vero, ma gli avvisi possono generare tonnellate di rumore (ad es. Da gemme che stai usando che non funzionano senza avvisi). Inoltre, preferisco NoMethodErrorricevere un avviso, ma YMMV.
Myron Marston,

1
@ Jwan622: potresti iniziare scrivendo un esempio, che ha foo = Foo.new(...)e quindi gli utenti foonelle righe successive. Successivamente, scrivi un nuovo esempio nello stesso gruppo di esempio che ha anche bisogno di Fooun'istanza nello stesso modo. A questo punto, si desidera refactoring per eliminare la duplicazione. Puoi rimuovere le foo = Foo.new(...)linee dai tuoi esempi e sostituirle con un let(:foo) { Foo.new(...) }w / o cambiando il modo in cui gli esempi usano foo. Ma se fai il refactoring before { @foo = Foo.new(...) }devi anche aggiornare i riferimenti negli esempi da fooa @foo.
Myron Marston,

82

La differenza tra l'utilizzo di variabili di istanze ed let()è che let()viene valutata in modo pigro . Ciò significa che let()non viene valutato fino a quando il metodo che definisce non viene eseguito per la prima volta.

La differenza tra beforee letè che let()ti dà un bel modo di definire un gruppo di variabili in uno stile "a cascata". In questo modo, le specifiche sembrano un po 'migliori semplificando il codice.


1
Capisco, è davvero un vantaggio? Il codice viene eseguito per ogni esempio, indipendentemente.
inviato il

2
È più facile da leggere IMO e la leggibilità è un fattore enorme nei linguaggi di programmazione.
Mike Lewis,

4
Senthil - in realtà non è necessariamente eseguito in tutti gli esempi quando si utilizza let (). È pigro, quindi viene eseguito solo se è referenziato. In generale, questo non ha molta importanza perché il punto di un gruppo di esempio è quello di avere diversi esempi eseguiti in un contesto comune.
David Chelimsky,

1
Quindi questo significa che non dovresti usare letse hai bisogno di qualcosa da valutare ogni volta? ad esempio, ho bisogno che un modello figlio sia presente nel database prima che venga attivato un comportamento sul modello genitore. Non faccio necessariamente riferimento a quel modello figlio nel test, perché sto testando il comportamento dei modelli genitore. Al momento sto usando il let!metodo invece, ma forse sarebbe più esplicito inserire quella configurazione before(:each)?
gar

2
@gar - Userei una Factory (come FactoryGirl) che ti consente di creare quelle associazioni figlio richieste quando crei un'istanza del genitore. Se lo fai in questo modo, non importa se usi let () o un blocco di installazione. Let () è utile se non hai bisogno di usare TUTTO per ogni test nei tuoi contesti secondari. L'installazione dovrebbe avere solo ciò che è richiesto per ognuno.
Harmon

17

Ho completamente sostituito tutti gli usi delle variabili di istanza nei miei test rspec per usare let (). Ho scritto un esempio veloce per un amico che lo ha usato per insegnare una piccola classe Rspec: http://ruby-lambda.blogspot.com/2011/02/agile-rspec-with-let.html

Come alcune delle altre risposte qui dicono, let () viene valutato in modo pigro, quindi caricherà solo quelle che richiedono il caricamento. ASCIUGA le specifiche e le rende più leggibili. In effetti ho portato il codice let () Rspec da usare nei miei controller, nello stile di gem eredited_resource. http://ruby-lambda.blogspot.com/2010/06/stealing-let-from-rspec.html

Oltre alla valutazione pigra, l'altro vantaggio è che, in combinazione con ActiveSupport :: Preoccupazione e le specifiche / supporto / comportamento load-everything-in, è possibile creare specifiche mini-DSL specifiche per la propria applicazione. Ne ho scritti uno per i test su risorse Rack e RESTful.

La strategia che uso è Factory-everything (tramite Machinist + Forgery / Faker). Tuttavia, è possibile usarlo in combinazione con blocchi precedenti (: each) per precaricare le fabbriche per un intero set di gruppi di esempi, consentendo alle specifiche di funzionare più velocemente: http://makandra.com/notes/770-taking-advantage -di-RSpec-s-let-in-prima-blocchi


Ehi, Ho-Sheng, in realtà ho letto molti dei tuoi post sul blog prima di porre questa domanda. Per quanto riguarda la tua # spec/friendship_spec.rbe # spec/comment_spec.rbad esempio, non credi che rendono meno leggibile? Non ho idea di da dove usersvenga e avrò bisogno di scavare più a fondo.
inviato il

La prima dozzina di persone a cui ho mostrato il formato lo trovano molto più leggibile e alcuni di loro hanno iniziato a scriverlo. Ora ho abbastanza codice di specifica usando let () che ho riscontrato anche alcuni di questi problemi. Mi ritrovo nell'esempio e, partendo dal gruppo di esempio più interno, mi rialzo. È la stessa abilità di usare un ambiente altamente meta-programmabile.
Ho-Sheng Hsiao,

2
Il più grande gotcha in cui mi sono imbattuto sta usando accidentalmente let (: subject) {} invece di subject {}. subject () è impostato in modo diverso da let (: subject), ma let (: subject) lo sovrascriverà.
Ho-Sheng Hsiao,

1
Se riesci a lasciar andare il "drill down" nel codice, troverai la scansione di un codice con dichiarazioni let () molto, molto più veloce. È più facile scegliere dichiarazioni let () durante la scansione del codice che trovare @variables incorporati nel codice. Usando @variables, non ho una buona "forma" per cui le linee si riferiscono all'assegnazione alle variabili e quali linee si riferiscono al test delle variabili. Usando let (), tutte le assegnazioni vengono eseguite con let () in modo da sapere "istantaneamente" dalla forma delle lettere in cui si trovano le dichiarazioni.
Ho-Sheng Hsiao,

1
Puoi rendere lo stesso argomento sul fatto che le variabili di istanza siano più facili da individuare, soprattutto perché alcuni editor, come il mio (gedit), evidenziano le variabili di istanza. Ho usato let()gli ultimi due giorni e personalmente non vedo alcuna differenza, tranne per il primo vantaggio menzionato da Myron. E non sono così sicuro di lasciar andare e cosa no, forse perché sono pigro e mi piace vedere il codice in anticipo senza dover aprire ancora un altro file. Grazie per i tuoi commenti
inviato il

13

È importante tenere presente che let è valutato in modo pigro e non inserisce metodi di effetti collaterali altrimenti non saresti in grado di cambiare facilmente da let a before (: each) . Puoi usare let! invece di lasciare in modo che venga valutato prima di ogni scenario.


8

In generale, let()è una sintassi più bella e ti fa risparmiare digitando @namesimboli ovunque . Ma, avvertitore emptor! Ho scoperto che let()introduce anche bug sottili (o almeno graffi alla testa) perché la variabile non esiste davvero fino a quando non si tenta di usarla ... Dillo al segno della storia: se l'aggiunta di un putsdopo let()per vedere che la variabile è corretta consente una specifica passare, ma senza le putsspecifiche non riesce - hai trovato questa sottigliezza.

Ho anche scoperto che let()non sembra cache in tutte le circostanze! L'ho scritto sul mio blog: http://technicaldebt.com/?p=1242

Forse sono solo io?


9
letmemorizza sempre il valore per la durata di un singolo esempio. Non memorizza il valore in più esempi. before(:all)al contrario, consente di riutilizzare una variabile inizializzata in più esempi.
Myron Marston,

2
se vuoi usare let (come ora sembra essere considerato la migliore pratica), ma hai bisogno di istanziare una particolare variabile immediatamente, ecco cosa let!è progettato per. relishapp.com/rspec/rspec-core/v/2-6/docs/helper-methods/…
Jacob

6

let è funzionale in quanto essenzialmente un Proc. Inoltre è memorizzato nella cache.

Un gotcha che ho trovato subito con let ... In un blocco Spec che sta valutando una modifica.

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

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

Dovrai essere sicuro di chiamare letal di fuori del tuo blocco di attesa. cioè stai chiamando FactoryGirl.createnel tuo blocco let. Di solito lo faccio verificando che l'oggetto sia persistente.

object.persisted?.should eq true

Altrimenti, quando il letblocco viene chiamato la prima volta, si verificherà effettivamente una modifica nel database a causa dell'istanza pigra.

Aggiornare

Sto solo aggiungendo una nota. Fai attenzione a giocare a code golf o in questo caso a rspec golf con questa risposta.

In questo caso, devo solo chiamare un metodo a cui l'oggetto risponde. Quindi invoco il _.persisted?metodo _ sull'oggetto come verità. Tutto quello che sto cercando di fare è creare un'istanza dell'oggetto. Potresti chiamare vuoto? o zero? pure. Il punto non è il test ma portare l'oggetto alla vita chiamandolo.

Quindi non puoi refactoring

object.persisted?.should eq true

essere

object.should be_persisted 

poiché l'oggetto non è stato istanziato ... è pigro. :)

Aggiornamento 2

sfruttare il let! sintassi per la creazione istantanea di oggetti, che dovrebbe evitare del tutto questo problema. Nota però che sconfiggerà molto lo scopo della pigrizia del let non sbattuto.

Inoltre, in alcuni casi potresti voler effettivamente sfruttare la sintassi del soggetto invece di lasciare in quanto ti potrebbe dare opzioni aggiuntive.

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

2

Nota a Joseph: se si stanno creando oggetti di database in un oggetto, before(:all)questi non verranno acquisiti in una transazione e sarà molto più probabile che si lascino la cruft nel proprio database di test. Usa before(:each)invece.

L'altro motivo per usare let e la sua valutazione pigra è che puoi prendere un oggetto complicato e testare singoli pezzi sovrascrivendo let in contesti, come in questo esempio molto ingegnoso:

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 prima (: tutti) dei bug hanno perso molti giorni del tempo dei nostri sviluppatori.
Michael Durrant,

1

"prima" di default implica before(:each). Ref The Rspec Book, copyright 2010, pagina 228.

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

Uso before(:each)per seminare alcuni dati per ciascun gruppo di esempio senza dover chiamare il letmetodo per creare i dati nel blocco "it". Meno codice nel blocco "it" in questo caso.

Uso letalcuni dati in alcuni esempi ma non in altri.

Sia prima che let sono ottimi per ASCIUGARE i blocchi "it".

Per evitare confusione, "let" non è lo stesso di before(:all). "Let" rivaluta il metodo e il valore per ciascun esempio ("it"), ma memorizza nella cache il valore su più chiamate nello stesso esempio. Puoi leggere di più qui: https://www.relishapp.com/rspec/rspec-core/v/2-6/docs/helper-methods/let-and-let


1

Voce dissenziente qui: dopo 5 anni di rspec non mi piace letmolto.

1. La valutazione lenta spesso confonde l'impostazione del test

Diventa difficile ragionare sulla configurazione quando alcune cose che sono state dichiarate nella configurazione non influenzano effettivamente lo stato, mentre altre lo sono.

Alla fine, per frustrazione qualcuno letpassa a let!(stessa cosa senza valutazione pigra) per far funzionare le loro specifiche. Se questo funziona per loro, nasce una nuova abitudine: quando una nuova specifica viene aggiunta a una suite più vecchia e non funziona, la prima cosa che lo scrittore prova è aggiungere scoppi a letchiamate casuali .

Molto presto tutti i vantaggi in termini di prestazioni sono spariti.

2. La sintassi speciale è insolita per gli utenti non rspec

Preferirei insegnare Ruby alla mia squadra piuttosto che i trucchi di rspec. Le variabili di istanza o le chiamate di metodo sono utili ovunque in questo progetto e in altri, la letsintassi sarà utile solo in rspec.

3. I "vantaggi" ci consentono di ignorare facilmente le modifiche alla progettazione

let()è buono per dipendenze costose che non vogliamo creare ancora e ancora. Si abbina anche bene con subject, consentendo di asciugare le chiamate ripetute ai metodi multi-argomento

Dipendenze costose ripetute più volte e metodi con grandi firme sono entrambi punti in cui potremmo migliorare il codice:

  • forse posso introdurre una nuova astrazione che isola una dipendenza dal resto del mio codice (il che significherebbe che sono necessari meno test)
  • forse il codice sotto test sta facendo troppo
  • forse ho bisogno di iniettare oggetti più intelligenti invece di un lungo elenco di primitivi
  • forse ho una violazione di "non chiedere"
  • forse il codice costoso può essere reso più veloce (più raro - attenzione all'ottimizzazione prematura qui)

In tutti questi casi, posso affrontare il sintomo di test difficili con un balsamo calmante di magia rspec, oppure posso provare a risolvere la causa. Mi sento come se avessi speso troppo degli ultimi anni nel primo e ora voglio un codice migliore.

Per rispondere alla domanda originale: preferirei non farlo, ma lo uso ancora let. Lo uso principalmente per adattarmi allo stile del resto della squadra (sembra che la maggior parte dei programmatori di Rails nel mondo siano ormai profondamente coinvolti nella loro magia rspec, quindi molto spesso). A volte lo uso quando aggiungo un test ad un codice di cui non ho il controllo o non ho il tempo di fare il refactoring per una migliore astrazione: cioè quando l'unica opzione è l'antidolorifico.


0

Uso letper testare le mie risposte HTTP 404 nelle mie specifiche API utilizzando i contesti.

Per creare la risorsa, io uso let!. Ma per memorizzare l'identificatore di risorsa, io uso let. Dai un'occhiata a come appare:

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

Ciò mantiene le specifiche pulite e leggibili.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.