«

»

Jan 24

Cucumber – Como testar aplicações Rails


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 :D

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

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 :D