
O primeiro contato que tive com o Cucumber foi procurando uma forma mais humana para escrever testes par aplicações Rails, o Cucumber tem uma integração perfeita com o Rails, agora vou mostrar como utilizar esta integração e por que o Cucumber é uma boa opção para escrever testes de integração para uma aplicação Ruby on Rails.
Eu procurava uma alternativa melhor que o RUnit para escrever testes de aceitação, por que até para mim, aqueles código pareciam muito mais atrelados ao como implementar do que ao que testar ….
Encontrei o RSpec, me apaixonei pelo RSpec story runner, e pouco tempo depois disto, o RSpec Story Runner foi descontinuado em favor do Cucumber.
Comecei a estudar e usar o Cucumber e ele logo se mostrou muito mais flexível e poderoso que o seu predecessor.
Como comecei a brincar com o cucumber utilizando o rails, acho que esta é uma boa forma de introduzi-lo aqui no blog também …
No post de introdção a BDD, foi apresentado um template de user story, o cucumber utiliza exatamente o mesmo template tanto pata as user stories quando para os cenários, onde as histórias serão detalhadas, então, antes de começar a programar, ou de utilizar o cucumber para automatizar a execução das nossas histórias, vamos escrever algumas histórias, começando apenas com as histórias definindo os recursos do sistema, vou escrever as histórias em inglês por que gosto de programar assim, mas serão histórias pequenas, fácil de traduzir se alguem tiver problemas em entender.
features/00_task_list.feature
1 2 3 4 | Feature: Task List
In order to remember what I need to do and what I have already completed
As a professional
I want to create a list of tasks, add tasks to the list and mark as completed the tasks I have completed |
Com esta história, podemos ver que o sistema que deveremos implementar é um gerenciador de tarefas, onde precisaremos cadastrar tarefas, e precisaremos marcar estass tarefas como concluidas. Podemos pensar em vários cenários para esta história, e para saber exatamente o que implementar precisaremos definir alguns cenários antes de começar com o código, por exemplo, precisamos de um cenário descrevendo os passos para adicionar uma tarefa, um para visualisar a listagem de tarefas, um para marcar uma tarefa como concluida, um para o usuário tentar adicionar uma tarefa repetida, a tarefa repetida já estando concluida e ainda não estando concluida, no primeiro caso podemos criar uma nova tarefa com a mesma descrição, no segundo caso o ideal é não duplicar a tarefa na lista.
Para facilitar a visualização disto, vou adicionar todos estes cenários ao arquivo da história, e depois vamos começar com o código, e automatizar um por um dos cenários.
features/01_task_list.feature
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 | Feature: Task List
In order to remember what I need to do and what I have already completed
As a professional
I want to create a list of tasks, add tasks to the list and mark as completed the tasks I have completed
Scenario: Add task
Given I am on my task list page
And I fill the task field with "my new task"
When I click "Add"
Then I should see "my new task" on the task list
Scenario: List tasks
Given I am on my task list page
And the vollowing tasks exists:
|title |done |
|my task |true |
|other task |false|
|one more task|false|
Then I should see the folloring tasks:
|Task |
|my task |
|other task |
|one more task|
And the line with text "my task" should have the class "done"
Scenario: Mark task as completed
Given I am on my task list page
And the vollowing tasks exists:
|title |done |
|my task |true |
|other task |false|
|one more task|false|
When I click on "Done" at "other_task_line"
Then the line with text "other task" should have the class "done"
Scenario: Duplicate completed task
Given I am on my task list page
And the vollowing tasks exists:
|title |done |
|my task |true |
|other task |false|
|one more task|false|
And I fill the task field with "my task"
When I click "Add"
Then I should see the folloring tasks:
|Task |
|my task |
|my task |
|other task |
|one more task|
Scenario: Duplicate open task
Given I am on my task list page
And the vollowing tasks exists:
|title |done |
|my task |true |
|other task |false|
|one more task|false|
And I fill the task field with "other task"
When I click "Add"
Then I should see the folloring tasks:
|Task |
|my task |
|other task |
|one more task|
And I should see "You cannot duplicate an open task" |
Para ficar claro, automatizar um cenário quer dizer configurar uma ferramentap ara que simule a ação de um usuário, a aplicação sera acessada como se um browser estivesse fazendo requisições, mas antes disto, vamos criar a aplicação rails com o comando padrão, como no exemplo abaixo:
1 | rails new cucumber_rails |
Para adicionar suporte a testes com o cucumber na aplicação, vamos fazer o seguinte, primeiro editamos o arquivo Gemfile da aplicação rails, tirando todos os comentários desnecessários e adicionando o cucumber como dependência:
cucumber_rails/Gemfile
1 2 3 4 5 6 7 8 9 10 11 | source 'http://rubygems.org' gem 'rails', '3.0.3' gem 'sqlite3-ruby', :require => 'sqlite3' group :development, :test do gem 'cucumber-rails' gem 'capybara' gem 'database_cleaner' gem 'ruby-debug' end |
Depois, dentro do diretório da aplicação Rails criada, executamos o comando:
1 | bundle install |
Depois disto, o cucumber já esta instalado, precisamos configurar o cucumber para que ele possa testar facilmente a aplicação Rails, já existe um gerador padrão para isto, basta executarmos os comandos:
1 2 | rails g cucumber:install rake db:migrate |
Isto vai criar a infra estrutura na aplicação para rodar o cucumber, entre esta infra estrutura encontram-se:
- tasks rake para execução dos testes:
- rake cucumber – um alias para cucumber:ok
- rake cucumber:all – executa todos os cenários
- rake cucumber:ok – executa os cenários que devem passar (não os WIP)
- rake cucumber:rerun – executa apenas os cenários que falharam
- rake cucumber:wip – executa os cenários que estão em desenvolvimento
- o arquivo config/cucumber.yml com as configurações padrão para cada uma das tasks mencionadas
- uma configuração extra de nome “cucumber” no arquivo database.yml permitindo configurações de banco de dados especificas se necessário
- o arquivo features/step_definitions/web_steps.rb com passos padrão para facilitar a escrita de cenários
- o arquivo features/support/paths.rb onde devem ser registrados os caminhos mencionados nos cenários
- o arquivo features/support/env.rb com configurações padrão do cucumber para testar uma aplicação rails
- e por último mas não menos importante, os diretórios:
- features – onde devem ficar os arquivos .feature com as user stories e os cenários
- features/support – onde devem ficar configurações e bibliotecas
- features/step_definitions – onde devem ficar as definições em ruby dos passos dos cenários
Antes de começarmos de verdade com o cucumber, vamos tentar entender como ele funciona, e como ele vai nos ajudar.
A proposta é tornar o texto dos cenários das user stories executáveis, para que a própria definição dos cenários, que segundo a definição do BDD devem ser granulares o suficiente para serem automatizados, possa se tornar o código do teste de aceitação automatizado.
Para que isto seja possível, o cucumber vai utilizar um pouco de código ruby e bastante expressões regulares para descobrir que método ruby deve ser executado para cada linha de texto.
Os passos de um cenário devem ser reutilizáveis entre cenários e se possível entre user stories, para isto eles devem ser bastante granulares, e também precisamos criar uma mini “DSL” para o dominio da aplicação que estamos projetando, por exemplo, sempre que quisermos clicar em um link, devemos escrever “I click on ‘…’”, evitando variações tipo “I`ll click on the link ‘…’”, assim só precisaremos de um método em Ruby para todos os passos de todos os cenários que precisarem clicar em algum link,
Esta “necessidade” de padronizar como as coisas serão escritas, vai fazer com que se torne impossível o cliente da aplicação escrever cenários para você, mas é bem fácil treinar o analista de negócios, ou o testador para fazerem isto de forma a reutilizar o máximo de código possível.
Voltando a nossa aplicação, vamos copiar o arquivo da user story com os cenários que escrevemos agora pouco para o diretório features da aplicação.
Copie o arquivo “features/01_task_list.feature” para o diretório “cucumber_rails/featres/task_list.feature”
Logo depois execute o comando:
1 | rake cucumber |
Verifique que o cumber vai reclamar que os passos dos cenários não estão implementados, mas como uma ótima ferramenta que é, ele é pró ativo, o cucumber já sugere o esqueleto que poderemos utilizar para implementar os passos, vejam que não temos exatamente 1 método para cada linha de texto, já temos algum código reutilizado, e com pequenos ajustes nas expressões regulares podemos reutilizar ainda mais código, como podem ver no arquivo abaixo.
features/task_list_step_definitions.rb
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | Given /^I fill the task field with "([^"]*)"$/ do |title| pending # express the regexp above with the code you wish you had end When /^I click "([^"]*)"$/ do |label| pending # express the regexp above with the code you wish you had end Then /^I should see "([^"]*)" on the task list$/ do |text| pending # express the regexp above with the code you wish you had end Given /^the vollowing tasks exists:$/ do |table| # table is a Cucumber::Ast::Table pending # express the regexp above with the code you wish you had end Then /^I should see the folloring tasks:$/ do |table| # table is a Cucumber::Ast::Table pending # express the regexp above with the code you wish you had end Then /^the line with text "([^"]*)" should have the class "([^"]*)"$/ do |text, class_name| pending # express the regexp above with the code you wish you had end When /^I click on "([^"]*)" at "([^"]*)"$/ do |label, enclosing| pending # express the regexp above with the code you wish you had end |
Coloquei o arquivo de definição dos steps no diretório raiz das features e não no diretório apropriado da aplicação para guardar o histórico, mas você pode colocar direto no diretório cucumber_rails/features/step_definitions, eu vou copiar o arquivo para lá.
Agora a idéia é implementar estes passos, para que o teste possa funcionar, o cucumber pode ser utilizado em diversos níveis diferentes da aplicação, mas neste caso, e principalmente para este tutorial, prefiro utilizar ele para os testes de aceitação apenas, vamos fazer com que o cucumber simule a ação de um browser acessando a aplicação. Para isto faremos as seguintes alterações:
Vamos registrar o caminho para “my tasks list page” no arquivo paths.rb, e remover os comentários desnecessários, o arquivo vai ficar assim:
cucumber_rails/features/support/paths.rb
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | module NavigationHelpers def path_to(page_name) case page_name when /the homes?page/ '/' when /my task list page/ '/' else begin page_name =~ /the (.*) page/ path_components = $1.split(/s+/) self.send(path_components.push('path').join('_').to_sym) rescue Object => e raise "Can't find mapping from "#{page_name}" to a path.n" + "Now, go and add a mapping in #{__FILE__}" end end end end World(NavigationHelpers) |
Como podem ver, direcionei para a raiz da aplicação, a idéia é que cada vez que alguem acessar a raiz da aplicação, uma lista de tarefas seja gerada e o browser seja redirecionado apra lá, depois a pessoa pode copiar a URL da lista criada e voltar aquela lista sempre que desejar.
Agora precisamos implementar os métodos que o cucumber solicitou, para isto vamos utilizar o capybara, que tem uma sintaxe bastante confortável para iteração com uma aplicação web, alem de poder acessar diretamente a aplicação rails com um servidor RACK interno ou automatizando um browser.
O código final vocês podem ver no arquivo abaixo, utilizei como exemplo o arquivo web_steps.rb que o gerador do rails criou.
cucumber_rails/features/step_definitions/task_list_step_definitions.rb
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 | Given /^I fill the task field with "([^"]*)"$/ do |title| fill_in('New Task:', :with => title) end Given /^I refresh the page$/ do current_path = URI.parse(current_url).path visit("#{current_path}?as") end When /^I click "([^"]*)"$/ do |label| click_button(label) end Then /^I should see "([^"]*)" on the task list$/ do |text| with_scope('div#task_list') do if page.respond_to? :should page.should have_content(text) else assert page.has_content?(text) end end end Given /^the vollowing tasks exists:$/ do |table| # table is a Cucumber::Ast::Table current_path = URI.parse(current_url).path tl_name = current_path[/([a-zA-Z0-9]+)$/] task_list = TaskList.where(:name => tl_name).first table.hashes.each do |hash| task = task_list.tasks.build hash task.save! end end Then /^I should see the folloring tasks:$/ do |expected_table| # table is a Cucumber::Ast::Table #current_table = table(tableish('table#task_list tr', 'td,th')) #expected_table.diff! current_table expected_table.hashes.each do |hash| text = hash[:Task] if page.respond_to? :should page.should have_content(text) else assert page.has_content?(text) end end end Then /^the line with text "([^"]*)" should have the class "([^"]*)"$/ do |text, class_name| with_scope("tr.#{class_name}") do if page.respond_to? :should page.should have_content(text) else assert page.has_content?(text) end end end When /^I click on "([^"]*)" at "([^"]*)"$/ do |label, enclosing| with_scope("tr##{enclosing}") do click_link(label) end end |
Algumas coisas importatnes neste código vou ressaltar na lista asseguir:
- O código escrito dentro do stub gerado pelo cucumber esta utilizando a API do capybara
- click_link, click_button e click_link_or_button são os métodos para gerar clicks
- fill_in preenche campos de texto
- choose seleciona campos tipo radio
- check e uncheck são utilizados para campos tipo checkbox
- select é utilizado para campos tipo select
- attach_file para simular uploads
- within(CSS selector ou XPath) restringe o escopo de qualquer um dos outros métodos
- find, find_field, find_link, find_button e all podem utilizar CSS selectors ou XPath para encontrar elementos e pode-se ler, setar o valor, clicar, …
- Mais informações sobre o capybara nesta URL: https://github.com/jnicklas/capybara
Agora, se executarmos os testes novamente veremos erros bem diferente, informando que a aplicação não funciona e que parte dela não funciona.
O ideal seria escrever um cenário, depois escrever o código para implementar este cenário, depois mais um cenário e assim por diante, mas pra facilitar a escrita do post resolvi implementar todos os cenários de uma só vez, já que o foco do post é mais o cucumber e a sua integração com o rails do que o TDD em si.
Agora para implementar a aplicação em si, antes de escrever algum código vou executar os seguintes comandos:
1 2 3 4 5 | del publicindex.html rails g controller welcome rails g model TaskList name:string rails g model Task task_list:belongs_to title:string done:boolean rails g controller tasks index |
Agora com todos os arquivos iniciais gerados pelo Rails já podemos começar a programar, vou primeiro corrigir algumas rotas, o meu arquivo de rotas ficou assim depois de toda a edição:
cucumber_rails/config/routes.rb
1 2 3 4 5 | CucumberRails::Application.routes.draw do resources :tasks, :path => "/:task_list" match 'task_lists/:task_list_id' => "task_lists#update", :via => :put root :to => "welcome#index" end |
Agora vamos corrigir um pouco a migration de lista de tarefas:
cucumber_rails/db/migrate/20110106010723_create_task_lists.rb
1 2 3 4 5 6 7 8 9 10 11 12 13 | class CreateTaskLists < ActiveRecord::Migration def self.up create_table :task_lists do |t| t.string :name, :null => false, :unique => true t.timestamps end end def self.down drop_table :task_lists end end |
E agora das tarefas:
cucumber_rails/db/migrate/20110106012244_create_tasks.rb
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | class CreateTasks < ActiveRecord::Migration def self.up create_table :tasks do |t| t.belongs_to :task_list, :null => false t.string :title, :null => false t.boolean :done t.timestamps end end def self.down drop_table :tasks end end |
E depois disto temos que rodar as migrations para criar o banco de dados:
1 | rake db:migrate |
Agora um pouco de código no controller welcome, a idéia é simplesmente criar uma lista de tarefas nova e redirecionar o usuário para esta lista, deve ser gerado um nome aleatório, se desejarmos mais tarde podemos escrever uma uer story permitindo a alteração deste nome, mas por enquanto o nome aleatório vai ser o suficiente, o código ficou assim:
cucumber_rails/app/controllers/welcome_controller.rb
1 2 3 4 5 6 7 8 9 10 | class WelcomeController < ApplicationController def index cnt = TaskList.count first_letter = ((cnt % 26) + 97).chr.upcase numeric = (1000 + cnt).to_s(15) task_list = TaskList.create :name => "#{first_letter}#{numeric}" redirect_to "/#{task_list.name}" end end |
Depois precisamos também de um pouco de código nos models para validar os dados, o model de listas de tarefas ficou assim:
cucumber_rails/app/models/task_list.rb
1 2 3 4 | class TaskList < ActiveRecord::Base has_many :tasks validates_presence_of :name end |
E o de tarefas assim:
cucumber_rails/app/models/task.rb
1 2 3 4 5 | class Task < ActiveRecord::Base belongs_to :task_list validates_presence_of :title validates_uniqueness_of :title, :scope => :task_list_id end |
Depois disto passei a editar os controllers, o de tarefas ficou assim:
cucumber_rails/app/controllers/tasks_controller.rb
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | class TasksController < ApplicationController respond_to :html, :js before_filter :load_task_list def index @tasks = @task_list.tasks @task = Task.new respond_with @tasks end def create @task = @task_list.tasks.build params[:task] save_and_prepare_answer render :action => 'index' end def update @task = Task.find params[:id] @task.done = true save_and_prepare_answer respond_with @task end private def load_task_list @task_list = TaskList.find_or_create_by_name(params[:task_list]) end def save_and_prepare_answer if @task.save @task = Task.new end load_task_list @tasks = @task_list.tasks end end |
Ele quase não tem código, mas a nossa aplicação é bastante simples mesmo, vamos utilizar muito javascript na view, mas somente o javascript não intrusivo do Rails no HTML, e algumas views gerando JS para a nossa implementação de AJAX. Para a aplicação começar a tomar forma, vamos começar a mexer nas views, eu criei apenas uma view, que vai conter a listagem de tarefas, e dois partials que vão fazer parte desta view e permitir a renderização via ajax, o código ajax vai ser executado quando os métodos create e update do controller forem chamados.
Vamos ver o código ajax primeiro, as duas views .erb.js são iguais então só vou colocar o código da primeira aqui:
cucumber_rails/app/views/tasks/create.js.erb
1 2 | $('new_task_form').update('<%= escape_javascript(render(:partial => 'new_task_remote')) %>');
$('task_list').update('<%= escape_javascript(render(:partial => 'task_table')) %>'); |
Esta view javascript renderiza dois partials, o primeiro para um formulário:
cucumber_rails/app/views/tasks/_new_task_remote.html.erb
1 2 3 4 5 6 7 8 | <%= form_for(@task, :task_list => @task_list.name) do |f| %> <%= f.label :title, "New Task:" %> <%= f.text_field :title %> <%= f.submit %> <% if @task.errors.any? %> <div id="error_explanation"> <%= @task.errors[:title].to_s %> </div> <% end %> <% end %> |
E uma para a tabela de tarefas:
cucumber_rails/app/views/tasks/_task_table.html.erb
1 2 3 4 5 6 7 8 9 10 11 12 | <table border="1" width="80%"> <caption>Listing of open and closed tasks</caption> <tr><th>Task</th><th>Done</th></tr> <% @tasks.each do |task| %> <tr id="<%= task.title.downcase.gsub(' ','_') %>_line" <%= 'class=done' if task.done %>> <td><%= task.title %></td> <td> <%= link_to_unless task.done, "Done", task_path(task, :task_list => @task_list.name), :remote => true, :method => :put %> </td> </tr> <% end %> </table> |
Estes partials juntos formam a view principal:
cucumber_rails/app/views/tasks/index.html.erb
1 2 3 4 5 6 7 8 9 10 11 | <div id="task_list_title" data-object="task_list" data-url="/task_lists/<%= @task_list.id %>"> Task List: <span class="rest_in_place" data-attribute="name" ><%= @task_list.name %></span> </div> <br/> <div id="new_task_form"> <%= render :partial => 'new_task_remote' %> </div> <br/> <div id="task_list"> <%= render :partial => 'task_table' %> </div> |
Com isto a aplicação esta completa, para que todos os testes funcionassem, alterar algumas configurações do cucumber, removi todos os comentários e mudifiquei as linhas 15, 16 e 18 do env.rb como pode ser visto abaixo:
cucumber_rails/features/support/env.rb
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | ENV["RAILS_ENV"] ||= "test" require File.expand_path(File.dirname(__FILE__) + '/../../config/environment') require 'cucumber/formatter/unicode' # Remove this line if you don't want Cucumber Unicode support require 'cucumber/rails/world' require 'cucumber/rails/active_record' require 'cucumber/web/tableish' require 'capybara/rails' require 'capybara/cucumber' require 'capybara/session' require 'cucumber/rails/capybara_javascript_emulation' # Lets you click links with onclick javascript handlers without using @culerity or @javascript require "selenium-webdriver" Capybara.default_selector = :css Capybara.default_driver = :selenium Capybara.default_wait_time = 5 ActionController::Base.allow_rescue = false Cucumber::Rails::World.use_transactional_fixtures = false if defined?(ActiveRecord::Base) begin require 'database_cleaner' DatabaseCleaner.strategy = :truncation rescue LoadError => ignore_if_database_cleaner_not_present end end |
Eu comecei a implementação do controller de lista de tarefas, mas como não criei nenhuma história para alteração do nome da lista de tarefas, não segui com a implementação.
Para a edição do nome da lista de tarefas ficar agradável, utilizei o plugin REST in Place, achei o resultado bastante agradável
Mas finalizar esta implementação fica por conta de vocês.
Comentários gerais
- A gem cucumber-rails faz a integração perfeita entre o rails e o cucumber, inclusive com alguns geradores para facilitar o trabalho
- Boa parta da facilidade do trabalho se deve ao Capybara, vale a pena ler a documentação dele
- Para testar aplicações que utilizam bastante AJAX, como este exemplo simples que implementamos, é melhor configurar o capybara para utilizar um driver diferente do padrão (rack_test), no exemplo utilizamos o selenium, mas também é possível culerity e envjs
- Para facilitar a vida utilizando cucumber e Capybara, é extremamente importante conhecer CSS selectors e/ou XPath
- A maior parte do código Webrat é compativel com o Capybara
- O Culerity é uma boa opção caso você tenha o JRuby instalado na maquina
- Não consegui fazer o envjs funcionar no windows
Links e Downloads
- Código fonte da aplicação do exemplo.
- Cucumber
- Capybara
- WebDriver
- Cucumber-Rails
- capybara-envjs
- culerity
PS.: Se tiverem dúvidas sobre o uso do cucumber, ou comentários sobre este post, podem deixar nos comentários que vou responder o mais rápido possível

[...] Web [Translate] Continuando o assunto “Cucumber”, e dando uma sequencia ao post Como testar aplicações Rails, vamos falar um pouco agora sobre como utilizar o cucumber para testar aplicações web genéricas, [...]
[...] Web [Translate] Continuando o assunto “Cucumber”, e dando uma sequencia ao post Como testar aplicações Rails, vamos falar um pouco agora sobre como utilizar o cucumber para testar aplicações web genéricas, [...]
[...] tivemos uma introdução ao BDD, aprendemos como utilizar o cucumber para testar aplicações Rails e como utilizálo para testar aplicações web genéricas, agora vamos aplicar um pouco de BDD para [...]
Aqui testei com o Ruby 1.8.7 e 1.9, tive problema com ambos.
tu instalou o Ruby usando o RubyInstaller?
Sim, instalei o Ruby 1.8.7 usando o RubyInstaller, depois instalei outras versões através do pik. Será que o problema é o pik?
pode ser, eu não gostei muito do pik, achei ele muito crú ainda, tem coisas que o windows não facilita
Descobri o problema. Não era o pik, o Ruby estava instalado num diretório com espaços no nome, e DevKit se perdeu…