Kiedy używać RSpec let ()?

448

Zwykle używam przed blokami do ustawiania zmiennych instancji. Następnie używam tych zmiennych w moich przykładach. Niedawno się natknąłem let(). Według dokumentów RSpec jest do tego przyzwyczajony

... aby zdefiniować zapamiętaną metodę pomocnika. Wartość będzie buforowana dla wielu połączeń w tym samym przykładzie, ale nie w przykładach.

Czym różni się to od używania zmiennych instancji w blokach przed? A także kiedy powinieneś używać let()vs before()?

sent-hil
źródło
1
Niech bloki są leniwie oceniane, podczas gdy przed blokami działają przed każdym przykładem (są ogólnie wolniejsze). Używanie przed blokami zależy od osobistych preferencji (styl kodowania, makiety / kody pośredniczące ...). Niech bloki są zwykle preferowane. Możesz sprawdzić bardziej szczegółowe informacje o let
Nesha Zoric
Nie jest dobrą praktyką ustawianie zmiennych instancji przed przechwyceniem. Sprawdź betterspecs.org
Allison

Odpowiedzi:

605

Zawsze wolę letzmienną instancji z kilku powodów:

  • Zmienne instancji powstają, gdy się do nich odwołuje. Oznacza to, że jeśli grubo dotkniesz pisowni zmiennej instancji, zostanie utworzona i zainicjowana nowa nil, co może prowadzić do subtelnych błędów i fałszywych alarmów. Ponieważ lettworzy metodę, otrzymasz NameErrorbłąd, gdy ją przeliterujesz, co uważam za lepsze. Ułatwia to również refaktoryzację specyfikacji.
  • before(:each)Hak zostanie uruchomiony przed każdym przykładzie, nawet jeśli przykład nie korzysta z żadnego z tych zmiennych instancji określonymi w haku. Zwykle nie jest to wielka sprawa, ale jeśli konfiguracja zmiennej instancji zajmuje dużo czasu, to marnujesz cykle. W przypadku metody zdefiniowanej przez letkod inicjujący działa tylko wtedy, gdy wywołuje go przykład.
  • Możesz refaktoryzować zmienną lokalną w przykładzie bezpośrednio do let bez zmiany składni odwołania w przykładzie. Jeśli zmienisz fakturę na zmienną instancji, musisz zmienić sposób, w jaki odwołujesz się do obiektu w przykładzie (np. Dodaj an @).
  • Jest to nieco subiektywne, ale jak zauważył Mike Lewis, myślę, że ułatwia to odczytanie specyfikacji. Lubię organizację definiowania wszystkich moich zależnych obiektów leti utrzymywanie mojego itbloku ładnym i krótkim.

Powiązany link można znaleźć tutaj: http://www.betterspecs.org/#let

Myron Marston
źródło
2
Naprawdę podoba mi się ta pierwsza zaleta, o której wspomniałeś, ale czy możesz wyjaśnić nieco więcej na temat trzeciej? Do tej pory przykłady, które widziałem (specyfikacje mangi: github.com/mongoid/mongoid/blob/master/spec/functional/mongoid/... ) używają bloków jednowierszowych i nie widzę, jak brak „@” sprawia, że ​​jest to łatwiejsze do odczytania.
sent-hil
6
Jak powiedziałem, jest to trochę subiektywne, ale uważam, że pomocne jest letzdefiniowanie wszystkich zależnych obiektów i użycie before(:each)do skonfigurowania potrzebnej konfiguracji lub wszelkich próbnych / skrótów potrzebnych w przykładach. Wolę to od jednego dużego przed hakiem zawierającym to wszystko. Ponadto let(:foo) { Foo.new }jest mniej hałaśliwy (i bardziej do rzeczy) before(:each) { @foo = Foo.new }. Oto przykład tego, jak go używam: github.com/myronmarston/vcr/blob/v1.7.0/spec/vcr/util/…
Myron Marston
Dzięki za przykład, który naprawdę pomógł.
sent-hil
3
Andrew Grimm: prawda, ale ostrzeżenia mogą generować mnóstwo hałasu (tj. Z klejnotów, których używasz, które nie działają bez ostrzeżenia). Ponadto wolę dostawać NoMethodErrorostrzeżenie, ale YMMV.
Myron Marston
1
@ Jwan622: możesz zacząć od napisania jednego przykładu, który ma, foo = Foo.new(...)a następnie użytkowników foow późniejszych wierszach. Później piszesz nowy przykład w tej samej grupie przykładów, która również potrzebuje Fooinstancji w ten sam sposób. W tym momencie chcesz refaktoryzować, aby wyeliminować duplikację. Możesz usunąć foo = Foo.new(...)wiersze z przykładów i zastąpić je bez let(:foo) { Foo.new(...) }zmiany sposobu użycia przykładów foo. Ale jeśli zmienisz fakturę, before { @foo = Foo.new(...) }musisz także zaktualizować odniesienia w przykładach od foodo @foo.
Myron Marston
82

Różnica między używaniem zmiennych instancji a let()tym, że let()jest leniwa . Oznacza to, że let()nie jest oceniane, dopóki metoda, którą określa, nie zostanie uruchomiona po raz pierwszy.

Różnica między beforei letto, że let()daje dobry sposób na zdefiniowanie grupy zmiennych w stylu „kaskadowym”. W ten sposób specyfikacja wygląda nieco lepiej, upraszczając kod.

Mike Lewis
źródło
1
Rozumiem, czy to naprawdę zaleta? Kod jest uruchamiany dla każdego przykładu niezależnie od tego.
sent-hil
2
Łatwiej jest odczytać IMO, a czytelność jest ogromnym czynnikiem w językach programowania.
Mike Lewis
4
Senthil - w rzeczywistości nie musi działać w każdym przykładzie, gdy używasz let (). Jest leniwy, więc można go uruchomić tylko wtedy, gdy się do niego odwołuje. Ogólnie rzecz biorąc, nie ma to większego znaczenia, ponieważ celem grupy przykładów jest uruchomienie kilku przykładów we wspólnym kontekście.
David Chelimsky
1
Czy to oznacza, że ​​nie powinieneś używać, letjeśli za każdym razem potrzebujesz czegoś do oceny? np. potrzebuję modelu potomnego, który będzie obecny w bazie danych, zanim zostanie wywołane pewne zachowanie w modelu nadrzędnym. Niekoniecznie odnoszę się do tego modelu potomnego w teście, ponieważ testuję zachowanie modeli nadrzędnych. W tej chwili korzystam zlet! metody, ale może byłoby bardziej wyraźne, aby wprowadzić tę konfigurację before(:each)?
gar
2
@gar - użyłbym fabryki (takiej jak FactoryGirl), która pozwala tworzyć wymagane skojarzenia potomne podczas tworzenia instancji nadrzędnej. Jeśli zrobisz to w ten sposób, to tak naprawdę nie ma znaczenia, czy użyjesz let () lub bloku instalacyjnego. Let () jest dobry, jeśli nie musisz używać WSZYSTKIEGO dla każdego testu w swoich pod kontekstach. Instalator powinien mieć tylko to, co jest wymagane dla każdego z nich.
Harmon
17

Całkowicie zastąpiłem wszystkie zastosowania zmiennych instancji w moich testach rspec, aby użyć let (). Napisałem szybki numerek dla znajomego, który wykorzystał go do nauczania małej klasy Rspec: http://ruby-lambda.blogspot.com/2011/02/agile-rspec-with-let.html

Jak mówi niektóre inne odpowiedzi tutaj, let () jest leniwy, więc załaduje tylko te, które wymagają załadowania. DRY poprawia specyfikację i czyni ją bardziej czytelną. W rzeczywistości przeniosłem kod Rspec let () do użycia w moich kontrolerach, w stylu gem inherited_resource. http://ruby-lambda.blogspot.com/2010/06/stealing-let-from-rspec.html

Oprócz leniwej oceny, drugą zaletą jest to, że w połączeniu z ActiveSupport :: Concern oraz wczytywaniem / specyfikacją / wsparciem / zachowaniem możesz stworzyć własną specyfikację mini-DSL specyficzną dla twojej aplikacji. Napisałem te do testowania w stosunku do zasobów Rack i RESTful.

Używam strategii Fabrycznie wszystko (przez Machinist + Forgery / Faker). Możliwe jest jednak użycie go w połączeniu z blokami przed (: każdy), aby wstępnie załadować fabryki dla całego zestawu przykładowych grup, umożliwiając szybsze działanie specyfikacji: http://makandra.com/notes/770-taking-ainstage -of-rspec-s-let-in-before-blocks

Ho-Sheng Hsiao
źródło
Hej Ho-Sheng, przeczytałem kilka twoich postów na blogu, zanim zadałem to pytanie. Jeśli chodzi o twój # spec/friendship_spec.rbi # spec/comment_spec.rbprzykład, czy nie uważasz, że sprawiają, że jest mniej czytelny? Nie mam pojęcia, skąd userspochodzą i będę musiał kopać głębiej.
sent-hil
Około kilku pierwszych osób, które pokazałem format, są bardziej czytelne, a kilku z nich zaczęło z nim pisać. Mam już wystarczającą ilość kodu spec za pomocą let (), że napotykam też na niektóre z tych problemów. Przechodzę do przykładu i zaczynając od najgłębszej grupy przykładów, pracuję od nowa. Jest to ta sama umiejętność, co przy użyciu wysoce metaprogramowalnego środowiska.
Ho-Sheng Hsiao
2
Największy problem, na jaki wpadłem, to przypadkowe użycie let (: subject) {} zamiast tematu {}. Temat () jest skonfigurowany inaczej niż let (: subject), ale let (: subject) go zastąpi.
Ho-Sheng Hsiao
1
Jeśli możesz puścić „drążenie” w kodzie, wówczas skanowanie kodu za pomocą deklaracji let () jest znacznie, dużo szybsze. Łatwiej jest wybrać deklaracje let () podczas skanowania kodu, niż znaleźć @variables osadzone w kodzie. Korzystając z @variables, nie mam dobrego „kształtu”, dla którego linie odnoszą się do przypisywania do zmiennych, a które linie do testowania zmiennych. Za pomocą let () wszystkie zadania są wykonywane za pomocą let (), dzięki czemu wiesz „natychmiast” po kształcie liter, w których znajdują się twoje deklaracje.
Ho-Sheng Hsiao
1
Możesz wysunąć ten sam argument, że zmienne instancji są łatwiejsze do wybrania, zwłaszcza, że ​​niektóre edytory, takie jak moje (gedit) podświetlają zmienne instancji. Korzystałem let()z ostatnich kilku dni i osobiście nie widzę różnicy, z wyjątkiem pierwszej korzyści, o której wspomniał Myron. Nie jestem pewien, czy mogę odpuścić, a co nie, może dlatego, że jestem leniwy i lubię widzieć kod z góry bez konieczności otwierania kolejnego pliku. Dziękuję za komentarze.
sent-hil
13

Ważne jest, aby pamiętać, że let jest oceniany leniwie i nie wprowadza w nim metod skutków ubocznych, w przeciwnym razie nie byłbyś w stanie łatwo zmienić z let na wcześniej (: każdy) . Możesz użyć let! zamiast pozwolić , aby był oceniany przed każdym scenariuszem.

pisaruk
źródło
8

Ogólnie rzecz biorąc, let()jest to ładniejsza składnia i oszczędza pisania @namesymboli w dowolnym miejscu. Ale, zastrzegaczu! Odkryłem, że let()wprowadza również subtelne błędy (lub przynajmniej drapanie głowy), ponieważ zmienna tak naprawdę nie istnieje, dopóki nie spróbujesz jej użyć ... Powiedz znak opowieści: jeśli dodasz znak putspo, let()aby zobaczyć, że zmienna jest poprawna, pozwala na specyfikację przekazać, ale bez putsspecyfikacji zawodzi - znalazłeś tę subtelność.

Odkryłem również, że let()nie wydaje się buforować w każdych okolicznościach! Napisałem to na moim blogu: http://technicaldebt.com/?p=1242

Może to tylko ja?

Jon Kern
źródło
9
letzawsze zapamiętuje wartość na czas trwania jednego przykładu. Nie zapamiętuje wartości w wielu przykładach. before(:all), natomiast pozwala ponownie użyć zainicjowanej zmiennej w wielu przykładach.
Myron Marston
2
jeśli chcesz użyć let (jak obecnie wydaje się to uważane za najlepszą praktykę), ale potrzebujesz konkretnej zmiennej, aby natychmiast utworzyć instancję, właśnie do tego let!służy. relishapp.com/rspec/rspec-core/v/2-6/docs/helper-methods/…
Jacob
6

let jest funkcjonalny, ponieważ zasadniczo jest proc. Również jest buforowany.

Jeden problem, który znalazłem od razu z let ... W bloku Spec, który ocenia zmianę.

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

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

Musisz koniecznie zadzwonić letpoza oczekiwany blok. tzn. dzwonisz FactoryGirl.createw let Let Block. Zazwyczaj robię to, sprawdzając, czy obiekt jest utrwalony.

object.persisted?.should eq true

W przeciwnym razie, gdy letblok zostanie wywołany po raz pierwszy, zmiana w bazie danych rzeczywiście nastąpi z powodu opóźnionego tworzenia instancji.

Aktualizacja

Właśnie dodam notatkę. Ostrożnie graj golfa kodowego lub w tym przypadku rspec golfa z tą odpowiedzią.

W takim przypadku muszę po prostu wywołać metodę, na którą obiekt reaguje. Więc wzywam_.persisted? wywołuję metodę _ na obiekcie jako jego prawdziwość. Próbuję tylko utworzyć obiekt. Mógłbyś zadzwonić pusty? czy zero? też. Nie chodzi o test, ale o powołanie obiektu do życia, nazywając go.

Więc nie możesz refaktoryzować

object.persisted?.should eq true

być

object.should be_persisted 

ponieważ obiekt nie został utworzony ... jest leniwy. :)

Aktualizacja 2

wykorzystać let! składnia do natychmiastowego tworzenia obiektów, co powinno całkowicie uniknąć tego problemu. Zauważ jednak, że pokona on wiele celów lenistwa bez uderzeń.

Również w niektórych przypadkach możesz chcieć wykorzystać składnię tematu zamiast pozwolić, ponieważ może to dać dodatkowe opcje.

subject(:object) {FactoryGirl.create :object}
inżynierDave
źródło
2

Uwaga dla Josepha - jeśli tworzysz obiekty bazy danych w, before(:all)nie zostaną one przechwycone w transakcji i istnieje większe prawdopodobieństwo pozostawienia cruft w testowej bazie danych. Użyj before(:each)zamiast tego.

Innym powodem użycia let i jego leniwej oceny jest to, że możesz wziąć skomplikowany obiekt i przetestować poszczególne elementy, zastępując let w kontekstach, jak w tym bardzo wymyślonym przykładzie:

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
dotdotdotPaul
źródło
1
+1 przed (wszystkie) błędy zmarnowały wiele dni czasu naszych programistów.
Michael Durrant
1

Domyślnie „przed” oznacza before(:each). Odnośnik The Rspec Book, copyright 2010, strona 228.

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

Używam before(:each)do inicjowania niektórych danych dla każdej przykładowej grupy bez konieczności wywoływania letmetody tworzenia danych w bloku „it”. W tym przypadku mniej kodu w bloku „it”.

Używam, letjeśli chcę niektóre dane w niektórych przykładach, ale nie w innych.

Zarówno przed, jak i niech są świetne do OSUSZANIA bloków „it”.

Aby uniknąć nieporozumień, „let” to nie to samo, co before(:all). „Let” ponownie ocenia swoją metodę i wartość dla każdego przykładu („it”), ale buforuje wartość w wielu wywołaniach w tym samym przykładzie. Możesz przeczytać więcej na ten temat tutaj: https://www.relishapp.com/rspec/rspec-core/v/2-6/docs/helper-methods/let-and-let

konyak
źródło
1

Odmienny głos tutaj: po 5 latach rspec bardzo mi się nie podoba let.

1. Leniwa ocena często powoduje, że konfiguracja testu jest myląca

Trudno jest uzasadnić konfigurację, gdy niektóre rzeczy zadeklarowane w konfiguracji nie wpływają na stan, podczas gdy inne tak.

W końcu z frustracji ktoś po prostu zmienia się letna let!(to samo bez leniwej oceny), aby jego specyfikacja działała. Jeśli to zadziała, rodzi się nowy nawyk: kiedy nowa specyfikacja jest dodawana do starszego pakietu i nie działa, pierwszą rzeczą, którą pisze autor, jest dodanie grzywki do losowychlet połączeń.

Wkrótce zniknęły wszystkie korzyści związane z wydajnością.

2. Specjalna składnia jest nietypowa dla użytkowników spoza rspec

Wolę uczyć Ruby w moim zespole niż sztuczek rspec. Zmienne instancji lub wywołania metod są przydatne wszędzie w tym projekcie i innych, letskładnia przyda się tylko w rspec.

3. „Korzyści” pozwalają nam łatwo zignorować dobre zmiany w projekcie

let()jest dobry w przypadku kosztownych zależności, których nie chcemy tworzyć w kółko. Pasuje również dosubject , co pozwala wysuszyć powtarzające się wywołania metod wieloparametrowych

Drogie zależności powtarzane wiele razy, a metody z dużymi podpisami są punktami, w których moglibyśmy ulepszyć kod:

  • może mogę wprowadzić nową abstrakcję, która izoluje zależność od reszty mojego kodu (co oznaczałoby, że potrzeba mniej testów)
  • może testowany kod robi zbyt wiele
  • może muszę wstrzyknąć mądrzejsze obiekty zamiast długiej listy prymitywów
  • może mam pogwałcenie „nie pytaj”
  • może kosztowny kod może zostać przyspieszony (rzadsze - uwaga na przedwczesną optymalizację tutaj)

We wszystkich tych przypadkach mogę rozwiązać problem trudnych testów za pomocą kojącego balsamu magii rspec lub spróbować rozwiązać problem. Wydaje mi się, że spędziłem zbyt wiele z ostatnich kilku lat na tym pierwszym i teraz chcę trochę lepszego kodu.

Aby odpowiedzieć na pierwotne pytanie: wolałbym nie, ale nadal używam let. Używam go głównie do dopasowania się do stylu reszty zespołu (wygląda na to, że większość programistów Railsów na świecie jest teraz głęboko zanurzona w swojej magii rspec, więc to bardzo często). Czasami używam go, gdy dodam test do kodu, nad którym nie mam kontroli, lub nie mam czasu na refaktoryzację do lepszej abstrakcji: tj. Gdy jedyną opcją jest środek przeciwbólowy.

iftheshoefritz
źródło
0

Używam letdo testowania odpowiedzi HTTP 404 w specyfikacji API za pomocą kontekstów.

Aby utworzyć zasób, używam let!. Ale do przechowywania identyfikatora zasobu używam let. Zobacz, jak to wygląda:

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

Dzięki temu specyfikacje są czyste i czytelne.

Vinicius Brasil
źródło