10 porad o… Cucumber

W artykule o RSpecu obiecałem drugą część poświęconą mockom i stubom i do tego tematu jeszcze wrócimy. Ale póki co naszła mnie wena na Ogórka. Cucumber to – obok RSpeca właśnie – drugi filar BDD w Rubym i jeśli jeszcze go nie znasz – przyszła już najwyższa pora.
Pierwsze kroki z Ogórkiem pomogą postawić dwie pierwsze części naszego tutoriala.
Poniższe 10 wskazówek przyda się raczej osobom, które miały już wcześniej do czynienia z Cucumberem.
1. pickle
O pickle pisałem już wcześniej i będę pewnie trąbił o nim w nieskończoność. Jeśli masz wynieść z tego artykułu jedną rzecz – niech to będzie to. Przeczytaj, zainstaluj, korzystaj.
Pickle na GitHubie: http://github.com/ianwhite/pickle
2. Szkice scenariuszy i tła
Scenario outlines i Background to jedne z podstawowych funkcji Cucumbera. Nie zaszkodzi jednak powtórzyć. A nuż ktoś usłyszy o tym właśnie pierwszy raz.
Oba te patenty służą do zmniejszenia duplikacji w scenariuszach.
Scenario outline umożliwiają zastąpienie dwóch lub więcej podobnych scenariuszy jednym szkicem, do którego przekazujemy zestawy różnych opcji. Przykladowo zamiast:
Scenario: User signs in with invalid email address Given a user exists with email: "email@person.com", password: "password" When I go to the sign in page And I fill in "email" with "somewrongemail" And I fill in "password" with "password" And I press "Login" Then I should see "Invalid e-mail address" Scenario: User signs in with invalid password Given a user exists with email: "email@person.com", password: "password" When I go to the sign in page And I fill in "email" with "email@person.com" And I fill in "password" with "wrongpassword" And I press "Login" Then I should see "Invalid password"
Możemy napisać:
Scenario Outline: User signs in with invalid data
Given a user exists with email: "email@person.com", password: "password"
When I go to the sign in page
And I fill in "email" with "<email>"
And I fill in "password" with "<password>"
And I press "Login"
Then I should see "<message>"
Examples: Variations of invalid login data
| email | password | message |
| somewrongemail | password | Invalid e-mail address |
| email@person.com | wrongpassword | Invalid password |
Dla każdego przykładu z tabeli Examples Cucumber wykona pełen scenariusz, wypełniając bloki <email> itp. odpowiednimi wartościami.
Inną metodą optymalizacji długości feature’ów jest Background. Jeśli w scenariuszach w jednym pliku powtarza się spora ilość Givenów, czasem warto wydzielić je do jednego wspólnego bloku Background.
Wychodząc od początkowych 2 scenariuszy, możemy spróbować tak:
Background: Given a user exists with email: "email@person.com", password: "password" And I am on the sign in page Scenario: User signs in with invalid email address When I fill in "email" with "somewrongemail" And I fill in "password" with "password" And I press "Login" Then I should see "Invalid e-mail address" Scenario: User signs in with invalid password When I fill in "email" with "email@person.com" And I fill in "password" with "wrongpassword" And I press "Login" Then I should see "Invalid password"
Na początku każdego ze scenariuszy zostanie wywołany w całości blok Background, dzięki czemu efekt pozostaje taki sam.
3. Recycling kroków
Już od pierwszych wersji Cucumbera była możliwość wykorzystania istniejących kroków w definicjach innych. Możliwe było więc wykorzystanie np. całego zestawu web_steps przy definiowaniu własnych, bardziej złożonych, jednak format zapisu był dość niezręczny:
Given /^I log in as "([^\"]*)\/([^\"]*)"$/ do |email, password|
Given %q{I go to the login page}
Given %q{I fill in "E-mail" with "#{email}"}
Given %q{I fill in "Hasło" with "#{password}"}
Given %q{I press "Zaloguj się"}
end
Na szczęście wprowadzona w wersji 0.4.4 metoda steps znacznie uprzyjemnia korzystanie z tego mechanizmu:
Given /^I log in as "([^\"]*)\/([^\"]*)"$/ do |email, password|
steps %Q{
Given I go to the login page
And I fill in "E-mail" with "#{email}"
And I fill in "Hasło" with "#{password}"
And I press "Zaloguj się"
}
end
4. Popracuj nad regexami
Sporą bolączką większych projektów jest duplikacja kodu. Dużo trąbi się o DRY ale pewna ilość redundancji i tak wkrada się do programów. Nie inaczej jest z testami. Dzięki temu, że kroki Cucumbera definiujemy przy użyciu wyrażeń regularnych, mamy do dyspozycji potężne narzędzie do DRY-owania testów.
Czy na pewno potrzebujesz oddzielne definicje dla Given I am logged in i Given an admin signs in? A co z krokami typu Given I am logged in with email: "lukasz@czak.pl"? Jeśli dobrze przemyślisz regexa, wszystkie możesz zdefiniować w jednym kroku, np:
Given /^I am logged in(?: as (\w+))?(?: with email "([^\"]*)")?$/do |role, email|
role ||= "user"
email ||= "lukasz@czak.pl"
password = "secret"
steps %Q{
Given a #{role}: "me" exists with email: "#{email}", password: "#{password}"
And I go to the login page
And I fill in "E-mail" with "#{email}"
And I fill in "Password" with "#{password}"
And I press "Login"
}
end
Taka definicja pasuje do kroków w kilku postaciach. Jeśli potrzebujesz w scenariuszu zalogowanego usera (bez wymagań co do jego roli czy emaila) możesz zapisać po prostu:
Given I am logged in
Jeśli potrzebujesz usera w konkretnej roli:
Given I am logged in as admin Given I am logged in as user
Kiedy potrzebujesz usera z konkretnym emailem (który np. wykorzystujesz w dalszej części scenariusza):
Given I am logged in with email: "ziom@czak.pl"
I wreszcie aby stworzyć usera w konkretnej roli i ze specyficznym adresem e-mail:
Given I am logged in as admin with email: "lukasz@czak.pl"
Cała magia powyższej definicji zawiera się w bloku postaci (?: as (\w+))?. Dzięki niemu człon as admin czy as user zostanie zaakceptowany, a odpowiednia rola przechwycona do zmiennej role. Jednocześnie dzięki ? na końcu cały ten blok jest opcjonalny.
Oczywiście z wyrażeniami regularnymi można tworzyć jeszcze większe cuda – to tylko jeden przykład.
5. Debugger
Długo zbywałem debugger jako narzędzie mało w Rubym przydatne, ale ostatnio przekonuję się do niego coraz bardziej. Kiedy już nie mam pomysłu dlaczego test się wysypuje, wstawiam do niego krok:
Then debug
..a w debug_steps.rb czeka definicja:
Then /^debug$/ do debugger end
Pozostaje tylko odpalić scenariusz i pozwolić debuggerowi we wskazanym miejscu zatrzymać wykonywanie.
6. Before, After i tagi
Cucumber, na wzór RSpeca, posiada metody Before i After, dzięki którym możemy zdefiniować operacje wykonywane przed lub po każdym scenariuszu. Dzięki połączeniu z tagami może się to nam przydać do estetycznego uporządkowania ogórków.
Może się zdarzyć tak, że część naszych scenariuszy wymaga pewnych danych do działania. Z uporem maniaka wstawiamy więc do każdego z nich:
Given a page exists with slug: "hello" And a blog post exists with permalink: "hello"
Jeśli powtarza się to w kilku scenariuszach, możemy nadać im tag, np @seed:
@seed Scenario: User reads the "Hello" page
a w support/seed_data.rb wstawić:
Before("@seed") do
Factory(:page, :slug => "hello")
Factory(:blog_post, :permalink => "hello")
end
Teraz jeśli będzie potrzebny jeszcze jeden scenariusz wymagający tego samego – wystarczy nadać mu odpowiedni tag.
7. Oszukać delayed_joba
Delegowanie długich zadań do delayed_job jest już pewnym standardem, jednak testowanie takich asynchronicznych mechanizmów wciąż nie. Najprostsze rozwiązanie to małe oszustwo:
Given /the system processes delayed jobs/ do Delayed::Job.work_off end
W żądanym miejscu scenariusza wstawiamy:
... And the system processes delayed jobs ...
I mamy pewność, że cała dotychczasowa kolejka delayed_joba zostanie wykonana.
Wszystkie biblioteki wykonujące zadania w tle mają synchroniczną metodę wykonującą wszystko i można wykorzystać je analogicznie.
8. Poznaj cucumber.yml
Plik konfiguracyjny Cucumbera – config/cucumber.yml jest dosyć rozbudowany na „dzień dobry”, ale warto przyjrzeć mu się mimo wszystko i z kilku rzeczy skorzystać.
Pierwsza rzecz to --format, który może przyjąć 10 wartości, ale w domyślnej konfiguracji mamy do czynienia tylko z dwoma.
Głównym elementem cucumber.yml są jednak profile. Na starcie otrzymujemy dwa: default i wip. Jeśli w scenariuszach korzystamy aktywnie z tagów, warto utworzyć dodatkowy profil, który będzie uruchamiał jedynie te oznaczone (lub nie oznaczone) wskazanymi.
Wychodząc od domyślnego:
default: <%= std_opts %> wip: --tags @wip:3 --wip features
Często dodaję dodatkowy profil javascript i zmieniam układ na:
default: <%= std_opts %> --tags ~@javascript wip: --tags @wip:3 --wip features javascript: <%= std_opts %>
Dzięki temu „normalne”, regularne uruchamianie ogórków pomija długo wykonujące się scenariusze wymagające @javascript, zaś uruchomienie pełnego zestawu – wraz z Selenium/Culerity wymaga wywołania:
cucumber --profile javascript
9. Pamiętaj o supporcie
Pamiętaj, że każdy plik z features/support jest wczytywany przed wykonaniem całego zestawu testów. Nie ma więc potrzeby zapychać features/support/env.rb, skoro można np. dodać plik features/support/capybara.rb, a w nim:
Capybara.default_driver = :rack_test Capybara.javascript_driver = :culerity
10. Pożegnanie z ogórkiem
Nie mniej ważną umiejętnością od wszystkich powyższych jest świadoma rezygnacja z Cucumbera i wybór lepszego narzędzia do określonego celu.
Modelowym przykładem jest testowanie integracji z zewnętrznymi usługami, np. z systemami płatności. Choć może kusić wysoki poziom abstrakcji Cucumbera i przeświadczenie, że „może wszystko to co i user”, bez spec’ów kontrolerów się w tym wypadku nie obejdzie.




Hubert Łępicki 6.07.2010
11. http://github.com/cavalle/steak
Wywalić cucumbera i użyć czegoś prostszego!
martinciu 6.07.2010
http://github.com/martinciu/pickle-mongoid – jesli uzywasz mongoid
tjeden 6.07.2010
Skoro alternatywa dla ogórka, tu musi mieć w nazwie mięso.
Dużo zyskuje się na szybkości i traci na czytelności migrując z ogórka na stek?
Piotr Sarnacki 6.07.2010
Przy testach integracyjnych narzut ogórka jest prawdopodobnie dość mały, więc nie liczyłbym na lepszą wydajność.
tjeden 6.07.2010
Chodziło mi o szybkość pisania.
Problem wydajności przestał istnieć, odkąd CI co godzinę puszcza wszystkie testy. Lokalnie odpalam tylko scenariusz nad którym pracuję. Wszystkie scenariusze włączam tylko do wielkiego święta czyli gdy jest spora zmiana funkcjonalności, albo ostry refaktoring.
Dodaj komentarz
TY