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 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.