Wprowadzenie do RSpec - BDD

Wysłane przez Krzysztof Kempiński dnia 18.09.2007

Chciałbym pokazać krótkie wprowadzenie do RSpec, dla wszystkich zainteresowanych rozpoczęciem pracy z tym narzędziem, jak również możliwościami wytwarzania oprogramowania w oparciu o Behaviour Driven Development.

1. Wprowadzenie

RSpec to framework służący testowaniu kodu w języku Ruby z wykorzystaniem metodologii Behaviour Driven Development (BDD). Behaviour Driven Development to technika tworzenia oprogramowania opierająca się na wcześniej przygotowanym opisie zachowania jego elementów składowych. Tak więc w odróżnieniu od Test Driven Development (TDD), nie używamyt "test case" i "test method", ale "behaviour" i "example".

2. Instalacja RSpec

RSpec funkcjonuje jako gem, więc można go zainstalować poleceniem:

gem install rspec

wymaga on interpretera Ruby w wersji conajmniej 1.8.4

3. Zaczynamy

W tutorialu skupimy się na opisie i stworzeniu klasy User, opisującej użytkownika, do którego może być dowiązane wiele ról. Zaczniemy od stworzenia katalogu dla naszego kodu:

mkdir rspec_tutorial
cd rspec_tutorial

4. Pierwszy przykład

RSpec dostarcza nam specyficzny Domain Specific Language, do opisu zachowania systemu, wraz z działającymi przykładami (behaviour, example). Na początku zapoznamy się z dwiema podstawowymi funkcjami: describe oraz it.

Stwórzmy plik user_spec.rb, który będzie zawierał opis testowanej przez nas klasy:
describe User do
end
w naszym przykładzie "describe User" oznacza w rzeczywistości "describe the behaviour of the User". Mamy już zręby naszego testu, możemy go uruchomić w konsoli:

spec user_spec.rb

program spec został zainstalowany wraz z instalacją gema rspec. Ma on wiele parametrów, które możemy wylistować wpisując po prostu:

spec

Po uruchomieniu naszego testu powinniśmy ujrzeć m.in. taki komunikat o błędzie:

./user_spec.rb:1: uninitialized constant User (NameError)

Nasz test skończył się błędem, co oznacza że test nie zakończył się poprawnie. Widzimy dlaczego - nie stworzyliśmy przecież wcześniej klasy, którą chcemy testować. Tworzymy zatem plik user.rb:
class User
end
i dołączamy go do testu:
require 'user'

describe User do
end
teraz po uruchomieniu testu powinniśmy otrzymać:

$ spec user_spec.rb

Finished in 6.0e-06 seconds

0 examples, 0 failures


wynik pokazuje, iż nie mamy żadnych przykładów, które są częścią BDD (example). Spróbujmy je dodać. Przykłady dodajemy za pomocą funkcji "it" poprzedzonej nazwą przykładu:
describe User do
  it "should be in any roles assigned to it" do
  end
end
Ponownie uruchomimy test, używając formatu wyjściowego SpecDoc:

$ spec user_spec.rb --format specdoc

User
- should be in any roles assigned to it

Finished in 0.022865 seconds

1 example, 0 failures


Na ekranie zobaczymy wszystkie zachowania (behaviour), czyli obiekty stworzone za pomocą "describe", oraz ich przykłady, stworzone poprzez "it".
Jednak nasz dotychczasowy przykład w zasadzie nic nie robi. Spróbujemy go wypełnić kodem:
describe User do
  it "should be in any roles assigned to it" do
    user.should be_in_role("assigned role")
  end
end
... i uruchomić ...

$ spec user_spec.rb --format specdoc

User
- should be in any roles assigned to it (ERROR - 1)

1)
NameError in 'User should be in any roles assigned to it'
undefined local variable or method `user' for #<#<Class:0x14ed15c>:0x14ecdd8>
./user_spec.rb:6:

Finished in 0.017956 seconds

1 example, 1 failure


Warto zauważyć kilka rzeczy. Po pierwsze tekst "(ERROR – 1)" mówi nam, że wystąpił błąd w "should be in any roles assigned to it". "1" mówi, że dokładny opis możemy szukać podniżej pod "1)". Ma to znaczenie przy większej ilości przykładów.

Kolejną kwestią jest brak backtrace błędu, co możemy wymusić parametrem --backtrace programu spec.

Mając na uwadze błąd, mówiący, że nie ma takiego usera, uzupełniamy nasz kod:
describe User do
  it "should be in any roles assigned to it" do
    user = User.new
    user.should be_in_role("assigned role")
  end
end
... i uruchamiamy test ...
$ spec user_spec.rb --format specdoc

User
- should be in any roles assigned to it (ERROR - 1)


1)
NoMethodError in 'User should be in any roles assigned to it'
undefined method `in_role?' for #<User:0x14ec8ec>
./user_spec.rb:7:

Finished in 0.020779 seconds

1 example, 1 failure


błąd mówi, ze user nie odpowiada na akcję in_role?. Zatem trzeba uzupełnić kod o taką funkcję:
class User
  def in_role?(role)
  end
$ spec user_spec.rb --format specdoc

User
- should be in any roles assigned to it (FAILED - 1)


1)
'User should be in any roles assigned to it' FAILED
expected in_role?("assigned role") to return true, got nil
./user_spec.rb:7:


Finished in 0.0172110000000001 seconds


1 example, 1 failure


Kolejny raz wynik testu mówi nam o niepoprawnym działaniu klasy. in_role? zwraca nil. Kolejny raz modyfikacja i uruchomienie:
class User
  def in_role?(role)
    true
  end
$ spec user_spec.rb --format specdoc

User
- should be in any roles assigned to it


Finished in 0.018173 seconds

1 example, 0 failures


Wróćmy jeszcze raz do naszego przykładu:
describe User do
  it "should be in any roles assigned to it" do
    user = User.new
    user.should be_in_role("assigned role")
  end
end
implementacja nie wykonuje tego co opisuje przykład - "should be in any roles assigned to it" ale u nas nie ma jeszcze żadnej dowiązanej roli do użytkownika. Wykonajmy kilka poprawek:
describe User do
  it "should be in any roles assigned to it" do
    user = User.new
    user.assign_role("assigned role")
    user.should be_in_role("assigned role")
  end
end
$ spec user_spec.rb --format specdoc

User
- should be in any roles assigned to it (ERROR - 1)


1)
NoMethodError in 'User should be in any roles assigned to it'
undefined method `assign_role' for #<User:0x14ec784>
./user_spec.rb:6:


Finished in 0.018564 seconds

1 example, 1 failure


no tak zapomnieliśmy zaimplementować funkcję assign_role:
class User
  def in_role?(role)
    true
  end

  def assign_role(role)
  end
end
$ spec user_spec.rb --format specdoc

User
- should be in any roles assigned to it


Finished in 0.018998 seconds

1 example, 0 failures


Udało się! Kod został przetestowany pod kątem zachowania mówiącego o dowiązaniu do roli. Jednak ujmując nasz kod w ramach BDD brakuje nam jeszcze przetestowania innych możliwych zachowań, nim kod oddamy dalej.

5. Drugi przykład


Jak do tej pory w naszym przykładzie użytkownik mówi nam "tak" jeśli zapytamy go o przynależność do roli, do której został przypisany. Chcielibyśmy jeszcze, żeby mówił "nie" gdy zapytamy go o rolę, do której nie należy:
describe User do
  it "should be in any roles assigned to it" do
    user = User.new
    user.assign_role("assigned role")
    user.should be_in_role("assigned role")
  end

  it "should NOT be in any roles not assigned to it" do
  end
end
$ spec user_spec.rb --format specdoc

User
- should be in any roles assigned to it
- should NOT be in any roles not assigned to it


Finished in 0.018231 seconds

2 examples, 0 failures


wypełnimy duży przykład kodem:
describe User do
  it "should be in any roles assigned to it" do
    user = User.new
    user.assign_role("assigned role")
    user.should be_in_role("assigned role")
  end

  it "should NOT be in any roles not assigned to it" do
    user = User.new
    user.should_not be_in_role("unassigned role")
  end
end
$ spec user_spec.rb --format specdoc

User
- should be in any roles assigned to it
- should NOT be in any roles not assigned to it (FAILED - 1)


1)
'User should NOT be in any roles not assigned to it' FAILED
expected in_role?("unassigned role") to return false, got true
./user_spec.rb:12:


Finished in 0.019014 seconds

2 examples, 1 failure


Kolejny raz opis zachowania usera wytyka nam błędną implementację klasy User. Należy ją poprawić:
class User
  def in_role?(role)
    role == @role
  end

  def assign_role(role)
    @role = role
  end
end
$ spec user_spec.rb --format specdoc

User
- should be in any roles assigned to it
- should NOT be in any roles not assigned to it


Finished in 0.017194 seconds

2 examples, 0 failures


OK. Test zakończony pomyślnie. Ale po chwilowym zastanowieniu zauważysz zapewne, że ciągle coś jest nie tak z naszą klasą. Jeden z przykładów mówi: "should be in any roles assigned to it". Zatem user może należeć do wielu ról, a nie do jednej jak w naszej implementacji. Jest to dobry moment na refactoring i upewnienie się, czy Klient faktycznie potrzebuje dowiązania kilku ról do usera, czy też może obecna implementacja jest dla niego wystarczająca.

Artykuł opracowany na podstawie.