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.
