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.