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
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
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
require 'user' describe User do end
$ spec user_spec.rb
Finished in 6.0e-06 seconds
0 examples, 0 failures
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
$ spec user_spec.rb --format specdoc
User
- should be in any roles assigned to it
Finished in 0.022865 seconds
1 example, 0 failures
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
$ 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
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
$ 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
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
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
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
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
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
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
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
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
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.
