10 porad o... RSpec
RSpec to jedna najbardziej znanych nazw w ekosystemie Ruby’ego. Kluczowa biblioteka w arsenale BDD doskonale sprawdza się jako zastępca Test::Unit, a dzięki potężnemu DSLowi może być wygodnie stosowana w innych, również bardziej wysokopoziomowych kontekstach.
Wielu programistów nie zdaje sobie jednak sprawy z możliwości tego narzędzia. Dla wszystkich tych, którzy chcą z RSpeca wycisnąć jak najwięcej – dziś pierwszy odcinek serii “10 porad o… RSpec”.
1. Aliasy
Kilka kluczowych metod w DSLu RSpeca posiada przydatne aliasy, o których warto wiedzieć:
Metoda | Aliasy |
---|---|
example | it, specify |
double | mock, stub |
describe | context |
2. Podmiot (domyślny)
Przy użyciu metody subject
(a właściwie dwóch metod: ExampleGroupMethods#subject
i ExampleMethods#subject
) możemy określić i pobrać podmiot, którego będą dotyczyć przykłady w grupie. Zamiast typowego kodu:
describe Array do before(:each) do @array = Array.new end it "should be empty" do @array.should be_empty end end
możemy napisać:
describe Array do subject { Array.new } it "should be empty" do subject.should be_empty end end
Możemy pójść dalej. Jeśli badany obiekt odpowiada przekazanemu do metody describe
i konstruujemy go korzystając z konstruktora bez parametrów, możemy zrezygnować z bloku subject
:
describe Array do it "should be empty" do subject.should be_empty end end
3. Autogenerowane opisy przykładów
Powtarzanie w opisie “should be empty” tylko po to, by w samym przykładzie napisać “should be_empty”... może dałoby się bardziej DRY? Ależ owszem:
describe Array do specify { subject.should be_empty } end
Alias specify
wydaje się tu być bardziej na miejscu – “specify subject should be empty” brzmi bardziej po ludzku niż “it subject should be empty”. Ale czy nie straciliśmy przydatnego opisu przykładu?
Otóż nie! Jeśli nie podamy opisu jako parametru dla it()
(czy specify()
), RSpec spróbuje wygenerować automatyczny opis na bazie ostatniego warunku w przykładzie. W związku z tym powyższy przykład otrzyma całkowicie poprawny opis:
$ spec array_spec.rb --format nested Array should be empty Finished in 0.292932 seconds 1 example, 0 failures
Możemy wykonać jeszcze jeden krok i otrzymać najkrótszą, a wciąż równoważną formę zapisu tego samego przykładu. Jeśli weźmiemy pod uwagę, że wszystkie wywołania should/should_not/... bez jawnego odbiorcy są delegowane w tle właśnie do subject
, otrzymujemy:
describe Array do it { should be_empty } end
Krócej się już nie da. W pogoni za DRY i jak najkrótszymi formami warto jednak zawsze zastanowić się nad czytelnością przykładów.
4. Don’t worry, be_happy
Konstrukcja should be_empty
zwraca uwagę na kolejną magię RSpeca i jego potężne wykorzystanie method_missing
– automatyczne warunki dla predykatów.
Dzięki temu, że każdy obiekt klasy Array
posiada metodę empty?
, RSpec na jej podstawie może sprawdzić warunek should be_empty
. Całość odbywa się automatycznie – wszystkie predykaty (metody z ”?” na końcu), również te zdefiniowane przez nas, mogą być w ten sposób testowane.
Zamiast dość okrężnego:
person.happy?.should be_true
możemy – bez żadnej dodatkowej pracy z naszej strony – napisać równoważny warunek:
person.should be_happy
Kiedy warunek nie zostanie spełniony, drugi z przykładów wygeneruje na dodatek bardziej czytelny komunikat błędu:
expected happy? to return true, got false
RSpec zaakceptuje też warunki zaczynające się od be_a_
oraz be_an_
, dzięki czemu możemy w niektórych sytuacjach używać bardziej czytelne formy:
article.should be_a_recipe # sprawdzi article.recipe? user.should be_an_admin # sprawdzi user.admin?
5. Kolekcje
Jedna z ciekawszych możliwości RSpec to badanie kolekcji. Od prostych:
[1, 2, 3].should include(3) ["a", "b"].should include("a", "b")
...do ciekawszych:
class Person def children ["Ania", "Marek"] end end describe Person do it { should have(2).children } end
...aż do podejrzanych:
["Julia Roberts", "Brad Pitt"].should have(2).actors
Skąd actors
w klasie Array
??? Okazuje się, że to wywołanie jest tu tylko dla ozdoby. O ile przykład z dziećmi wykorzystuje kolekcję dostępną pod #children
i bada jej długość, to w ostatnim przykładzie have()
rozpoznaje, że ma do czynienia z kolekcją i bada ją samą pod kątem długości. actors
można tu podmienić na np. elements
i zachowanie pozostanie takie samo.
6. Współdzielone przykłady
Badając obiekty o podobnych interfejsach, możemy stworzyć zbiór przykładów odpowiednich dla każdego z nich. W duchu DRY, warto wydzielić je do oddzielnej grupy przykładów współdzielonych:
shared_examples_for "A collection" do it { should be_empty } end describe Array do it_should_behave_like "A collection" end describe Hash do it_should_behave_like "A collection" end
7. Zagnieżdżone grupy przykładów
Dzięki zagnieżdżeniu bloków describe
czy context
możemy znacznie zyskać na czytelności przykładów. Męczona przeze mnie specyfikacja Array
nabiera trochę więcej sensu z użyciem opisowych kontekstów:
describe Array do context "when first created" do it { should be_empty } end context "with a single element" do before { subject << "first" } it { should_not be_empty } end end
Zagnieżdżanie describe
zapewnia, że bloki before
będą wykonywane w oczekiwanej przez nas kolejności. Zapisując powyższy przykład z ich użyciem:
describe Array do before(:each) do # PIERWSZY @array = Array.new end context "with a single element" do before(:each) do # DRUGI @array << "first" end it "should not be empty" do # I W KOŃCU SAM PRZYKŁAD @array.should_not be_empty end end end
Przy uruchamianiu spec
warto skorzystać z opcji --format nested
i zwrócić uwagę na uporządkowanie przykładów dzięki sensownemu grupowaniu ich w kontekstach.
8. Pending
Oznaczenie przykładu jako pending
(oczekujący) stanowi dla programisty swego rodzaju “notatkę” – ten test należy napisać, poprawić, usunąć. Przydaje się kiedy zaczynamy pisać specyfikację i do głowy przychodzą nam kolejne właściwości, które chcemy badać. Warto w tym celu użyć metody it()
bez bloku:
describe Person do it "should be happy" it "should have a dog" it "should have 2 children" end
Wszystkie powyższe przykłady mają stan pending
i będą w raporcie oznaczone żółtym kolorem, dopóki nie zostaną zaimplementowane przez programistę.
Można również wykorzystać metodę pending
, np:
describe Person do it "should be happy" do pending "no money" subject.should be_happy end end
Wywołanie pending
na początku przykładu ignoruje dalsze operacje w bloku. Dzięki temu możemy tymczasowo zablokować test, który na pewno nie przechodzi poprawnie i w lepszej (zielono-żółtej) atmosferze dokończyć refactoring lub implementację innej funkcjonalności.
Jeszcze bardziej przydatne jest przekazanie bloku do metody pending
:
describe Person do it "should be happy" do pending "has no money, so it seems to fail" do subject.should be_happy end end end
Warunki w bloku są sprawdzane i jeśli nie zostaną spełnione, RSpec przejdzie do porządku dziennego z komunikatem “PENDING: has no money, so it seems to fail”. Jeśli jednak – z powodu wprowadzenia innej funkcjonalności lub zmiany – warunek zostanie spełniony, wtedy RSpec wyrzuci błąd. Ma to zwrócić uwagę developera na nagła zmianę stanu.
9. xit
Jeszcze szybszym sposobem na tymczasowe wyłączenie przykładu z testów jest zamiana metody it()
na xit()
(analogicznie specify
na xspecify
i example
na xexample
). Uwaga! Taki test nie zostanie oznaczony jako pending
. RSpec wyświetli jedynie krótki komunikat:
Example disabled: should be happy
10. Równość równości nierówna
W Rubym równość między obiektami możemy sprawdzić na cztery sposoby:
a == b a === b a.eql?(b) a.equal?(b)
Warto znać różnice między nimi (polecam ruby-doc.org) i wiedzieć, że RSpec umożliwia sprawdzanie równości przy użyciu każdej z nich:
a.should == b a.should === b a.should eql(b) a.should equal(b)
W następnym odcinku serii “10 porad o… RSpec” na warsztat bierzemy mocki i stuby.