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 %&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.



Komentarze

  1. Hubert Łępicki 6.07.2010

    Comment Arrow

    11. http://github.com/cavalle/steak

    Wywalić cucumbera i użyć czegoś prostszego! 😉


  2. martinciu 6.07.2010

    Comment Arrow

    http://github.com/martinciu/pickle-mongoid – jesli uzywasz mongoid


  3. tjeden 6.07.2010

    Comment Arrow

    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?


  4. Piotr Sarnacki 6.07.2010

    Comment Arrow

    Przy testach integracyjnych narzut ogórka jest prawdopodobnie dość mały, więc nie liczyłbym na lepszą wydajność.


  5. tjeden 6.07.2010

    Comment Arrow

    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.




O autorze

Łukasz Adamczak

Programuje w Rubym i dłubie w Rails od końca 2007. Przygody z Javą i PHP były krótkie, ale wystarczyły by z pasją i wzajemnością zakochać się w RoR. Wierny członek sekty nadgryzionego jabłka. Od niedawna adept tajemnej sztuki Objective-C.