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 %&gt
wip: --tags @wip:3 --wip features

Często dodaję dodatkowy profil javascript i zmieniam układ na:

default: <%= std_opts %&gt --tags ~@javascript
wip: --tags @wip:3 --wip features
javascript: <%= std_opts %&gt

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.