homeASCIIcasts

257: Especificaciones de petición y Capybara 

(view original Railscast)

Other translations: En Ja

Other formats:

Written by Juan Lupión

Es fundamental realizar tests de alto nivel a la hora de probar nuestras aplicaciones Rails. Una forma muy popular de efectuar este tipo de pruebas es Cucumber, que ya repasamos en el episodio 155 [verlo, leerlo]. Pero a la hora de definir el comportamiento de alto nivel de la aplicación la sintaxis de Cucumber no es del agrado de todo el mundo, por lo que en este episodio veremos una forma alternativa de hacer estos tests de alto nivel.

Aunque nosotros siempre recomendamos seguir un desarrollo guiado por tests, en este episodio vamos a ir añadiendo los tests sobre una aplicación ya escrita para no tener que preocuparnos de los detalles de implementación. Se trata de una sencilla aplicación que implementa una lista de tareas de una página. La aplicación muestra un listado de cosas pendientes de hacer y tiene un formulario que nos permite añadir nuevas tareas.

Nuestra sencilla aplicación de tareas pendientes.

Para probar esta aplicación vamos a utilizar especificaciones de petición (N. del T: request specs en el original), que están disponibles a partir de RSpec 2.0, por lo que lo primero que tenemos que hacer es añadir la gema RSpec Rails a los grupos de desarrollo y test en el Gemfile, y luego ejecutar bundle.

/Gemfile

source 'http://rubygems.org'

gem 'rails', '3.0.5'
gem 'sqlite3'
gem 'nifty-generators'

group :development, :test do
  gem 'rspec-rails'
end

Después de que Bundler se haya ejecutado ya podemos lanzar la siguiente orden para configurar RSpec en nuestra aplicación:

$ rails g rspec:install

Las especificaciones de petición son el equivalente en RSpec de los tests de integración que vienen con Rails (y que vimos en el episodio 187 [verlo, leerlo]). Para generar una especificación de petición invocamos al generador de tests de integración:

$ rails g integration_test task

Esto generará un fichero tasks_spec en el directorio spec/requests con el siguiente aspecto:

/spec/requests/tasks_spec.rb

require 'spec_helper'

describe "Tasks" do
  describe "GET /tasks" do
    it "works! (now write some real specs)" do
      # Run the generator again with the --webrat flag if you want to use webrat methods/matchers
      get tasks_path
      response.status.should be(200)
    end
  end
end

El contenido del fichero parece un test de RSpec normal pero tiene la diferencia de que podemos invocar métodos como get para cargar una página de la aplicación y response para comprobar la respuesta de dicha petición. La especificación que ha sido generada por defecto solicita la página de tareas pendientes que vimos antes, y comprueba que el valor de estado devuelto es 200, lo que indicaría que la petición ha tenido éxito. Esta petición debería pasar tal cual, por lo que podemos probarla. Ejecutemos rake spec:requests para pasar sólo las especificaciones de petición:

$ rake spec:requests
(in /Users/eifion/code/tasklist)
/Users/eifion/.rvm/rubies/ruby-1.9.2-p0/bin/ruby -S bundle exec rspec ./spec/requests/tasks_spec.rb
DEPRECATION WARNING: <% %> style block helpers are deprecated. Please use <%= %>. (called from _app_views_tasks_index_html_erb___875755388255758006_2152410020_3563250333774836596 at /Users/eifion/code/tasklist/app/views/tasks/index.html.erb:3)
.

Finished in 0.18535 seconds
1 example, 0 failures

Vemos que la especificación pasa, pero también se lanza un aviso de deprecación: este es el tipo de cosas que pasaríamos por alto si estuviéramos probando este tipo de funcionalidad sólo con el navegador. Por lo visto se nos ha olvidado utilizar un signo de igualdad en un helper de bloque.

En el fichero de vista del formulario veremos que se nos ha olvidado el símbolo de igualdad en la apertura de la etiqueta form_for, que es necesario en Rails 3.

/app/views/tasks/index.html.erb

<% form_for Task.new do |f| %>

Si añadimos esto...

/app/views/tasks/index.html.erb

<%= form_for Task.new do |f| %>

…y luego ejecutamos otra vez la especificaciones, veremos que esta vez no recibimos ningún aviso.

$ rake spec:requests
(in /Users/eifion/code/tasklist)
/Users/eifion/.rvm/rubies/ruby-1.9.2-p0/bin/ruby -S bundle exec rspec ./spec/requests/tasks_spec.rb
.

Finished in 0.16725 seconds
1 example, 0 failures

Nuestra primera especificación de petición de verdad

Por lo general en una especificación de petición queremos hacer algo más que comprobar el estado de la respuesta. Cambiemos la especificación por defecto por una que comprueba que se muestran las tareas pendientes en la página.

/specs/requests/tasks_spec.rb

require 'spec_helper'

describe "Tasks" do
  describe "GET /tasks" do
    it "displays tasks" do
      Task.create!(:name => "paint fence")
      get tasks_path
      response.body.should include("paint fence")
    end
  end
end

Esta especificación es muy sencilla. Creamos una nueva tarea y luego visitamos la página tasks para ver que el texto de la página incluye el nombre de la tarea. Cuando ejecutemos rake spec:requests veremos que la especificación pasa porque la página incluye dicho texto.

Las especificaciones de petición soportan todos los métodos de los tests de integración de Rails porque se basan en ellos. Por ejemplo, si queremos probar la creación de una nueva tarea podemos utilizar el método post_via_redirect para asegurarnos que se sigue la redirección cuando se crea una tare.

Ahora vamos a escribir esta especificación. Llamaremos a post_via_redirect para hacer un POST a la página index pasándole los parámetros necesarios para crear una nueva tarea llamada “mow lawn” (N. del T: cortar el césped) y luego comprueba que dicho texto aparece en la página de resultados.

/specs/requests/tasks_spec.rb

require 'spec_helper'

describe "Tasks" do
  
  # Other task omitted.
  
  describe "POST /tasks" do
    it "creates a task" do
      post_via_redirect tasks_path, ↵
	   :task => { :name => "mow lawn" }
      response.body.should include("mow lawn")
    end
  end
end

Si ejecutamos las especificaciones veremos que pasan dos, por lo que parece que nuestro código funciona como debería. Si hiciésemos desarrollo guiado por tests, empezaríamos con una especificación que falla y luego iríamos escribiendo el código necesario para que hacer que pasase (esta es una de las ventajas del desarrollo guiado por tests: nos aseguramos de que los tests de la aplicación siempre pasan). En esta aplicación hemos escrito directamente un test que pasa porque la funcionalidad ya estaba escrita. Cuando escribamos tests de código ya existente puede ser útil a veces romper algo en el código para asegurarnos de que el test que hemos escrito no deja pasar dicho error.

Pruebas de Interfaz con Capybare

El problema que tienen nuestras especificaciones de petición es que no comprueban toda la experiencia de usuario. Podríamos romper por completo el formulario para una Nueva Tarea y sin embargo nuestras especificaciones no se darían cuenta de esta situación, porque estamos haciendo directamente un POST a la acción create del servidor, en lugar de ir desde el formulario (que es lo que haría un usuario).

Tenemos que reproducir las acciones del usuario, lo que podemos hacer usando Capybara que es una alternativa a Webrat, el cual vimos en el episodio 156 [verlo, leerlo]. Con Capybara tenemos métodos para reproducir el comportamiento de un usuario en una aplicación web. Como se trata de una gema, tenemos que añadirla de la manera habitual. También añadiremos la gema launchy (en breve veremos por qué). En el Gemfile vamos a añadir ambas gemas a los grupos de desarrollo y test, ejecutando luego bundle para instalarlas.

/Gemfile

source 'http://rubygems.org'

gem 'rails', '3.0.5'
gem 'sqlite3'
gem 'nifty-generators'

group :development, :test do
  gem 'rspec-rails'
  gem 'capybara'
  gem 'launchy'
end

Las especificaciones de petición incluyen automáticamente Capybara por lo que en la primera de nuestras especificaciones podemos utilizar el método visit de Capybar en lugar de get. También podemos cambiar reponse.body.should include por page.should have_content.

En la segunda especificación podemos utilizar Capybara para hacer como si el usuario rellenase y enviase el formulario en lugar de hacer un POST directamente a la acción create. Con el método fill_in podemos encontrar la caja de texto con la etiqueta correspondiente y establecer su valor, tras lo que podemos utilizar click_button para encontrar el botón del formulario y hacer clic en él.

/specs/requests/task_spec.rb

require 'spec_helper'

describe "Tasks" do
  describe "GET /tasks" do
    it "displays tasks" do
      Task.create!(:name => "paint fence")
      visit tasks_path
      page.should have_content("paint fence")
    end
  end
  
  describe "POST /tasks" do
    it "creates a task" do
      visit tasks_path
      fill_in "Name", :with => "mow lawn"
      click_button "Add"
	 page.should have_content("Successfully added task.")
      page.should have_content("mow lawn")
    end
  end
end

Si ahora rompemos deliberadamente el formularios veremos que se captura el error.

Depuración de páginas

Si una especificación falla, ¿cómo podemos depurar este problema? Aquí es donde entra en juego la gema launchy. Como estamos usando Capybara podemos invocar al método save_and_open_page en cualquier punto, lo que hará que se abra la página en el navegador para poder examinarla. Vamos a añadirlo después del método click_button y cuando ejecutemos otra vez las especificaciones veremos la página justo antes de comprobar su contenido en el test.

La página a mitad de camino en la especificación.

Podemos ver el estado de la página en este punto, con el mensaje de flash visible y con la tarea que hemos añadido mediante Capybara.

Pruebas de JavaScript

Con esto ya tenemos la aplicación bien probada y nos ha sido muy fácil gracias a las especificaciones de petición y Capybara. Pero ¿y si nuestra aplicación tiene JavaScript y queremos probarlo? Pues es fácil.

Añadamos JavaScript a nuestra plantilla index para poder escribir algún test. Por simplicidad lo mostraremos en línea utilizando link_to_function para añadir un enlace en la página que invoca una función JavaScript que cambiará el texto del enlace a "js works". El script que se ejecuta requiere jQuery por lo que hemos añadido una referencia en el fichero de layout de la aplicación (por cierto, jQuery será la librería de JavaScript por defecto en Rails 3.1, en lugar de Prototype).

/app/views/tasks/index.html.erb

<h1>Task List</h1>

<%= link_to_function "test js", '$(this).html("js works")' %>

<%= form_for Task.new do |f| %>
  <p>
    <%= f.label :name %>
    <%= f.text_field :name %>
    <%= f.submit "Add" %>
  </p>
<% end %>

<ul>
  <% @tasks.each do |task| %>
    <li><%= task.name %></li>
  <% end %>
</ul>

Así que ahora ya tenemos un enlace en la página que dice "test js" y cuando hacemos clic en él dicho texto cambia.

El texto cambia después de hacer clic en el enlace.

Vamos a probar esta funcionalidad con Capybara. Primero escribimos una nueva especificación:

/spec/requests/tasks_spec.rb

it "supports js" do
  visit_tasks_path
  click_link "test js"
  page.should have_content("js works")
end

Nuestras especificaciones de petición fallarán:

$ rake spec:requests
(in /Users/eifion/code/tasklist)
/Users/eifion/.rvm/rubies/ruby-1.9.2-p0/bin/ruby -S bundle exec rspec ./spec/requests/tasks_spec.rb
.F.

Failures:

  1) Tasks GET /tasks supports js
     Failure/Error: page.should have_content("js works")
       expected #has_content?("js works") to return true, got false
     # ./spec/requests/tasks_spec.rb:14:in `block (3 levels) in <top (required)>'

Finished in 0.83232 seconds
3 examples, 1 failure
rake aborted!
ruby -S bundle exec rspec ./spec/requests/tasks_spec.rb failed

(See full trace by running task with --trace)

Esta especificación falla porque la página no tiene el texto que se pone mediante JavaScript. Capybara por defecto no soporta JavaScript, así que tenemos que decirle que utilice Selenium.

La funcionalidad que vamos a ver ahora no está soportada por la versión actual de Capybara pero con Bundler es muy fácil pasar a la última versión de GitHub.

/Gemfile

source 'http://rubygems.org'

gem 'rails', '3.0.5'
gem 'sqlite3'
gem 'nifty-generators'

group :development, :test do
  gem 'rspec-rails'
  gem 'capybara', :git => 'git://github.com/jnicklas/capybara.git'
  gem 'launchy'
end

Sólo tenemos que ejecutar bundle de nuevo para descargar la última versión de la gema de Capybara.

Otro paso que tenemos que dar para los tests de JavaScript es modificar el fichero spec_helper y añadir la línea require 'rspec/rails'.

/spec/spec_helper.rb

# This file is copied to spec/ when you run 'rails generate rspec:install'
ENV["RAILS_ENV"] ||= 'test'
require File.expand_path("../../config/environment", __FILE__)
require 'rspec/rails'
require 'capybara/rspec'

# resto del archivo...

Aunque probablemente este paso no sea necesario en la versión final, por ahora tenemos que hacerlo. Es fácil hacer que Capybara utilice un motor u otro de JavaScript en nuestros tests, lo único que tenemos que hacer es añadir la opción :js => true en la especificación.

/spec/requests/tasks_spec.rb

it "supports js", :js => true do
  visit tasks_path
  click_link "test js"
  page.should have_content("js works")
end

Ahora las especificaciones lanzarán Firefox y ejecutarán las pruebas que requieran JavaScript. Deberían pasar todas.

Se trata de una funcionalidad muy poderosa, pero nos podemos topar con un problema que tiene que ver con los registros de base de datos. Podemos demostrar este peligro añadiendo la opción :js => true a las primeras dos especificaciones, incluyendo la que crea una nueva instancia de Task.

/spec/requests/tasks_spec.rb

describe "Tasks" do
  describe "GET /tasks", :js => true do
    it "displays tasks" do
      Task.create!(:name => "paint fence")
      visit tasks_path
      page.should have_content("paint fence")
    end
    
    it "supports js" do
      visit tasks_path
      click_link "test js"
      page.should have_content("js works")
    end    
  end
end

Si ahora ejecutamos las especificaciones, utilizará Selenium con las dos primeras peticiones.

$ rake spec:requests
(in /Users/eifion/code/tasklist)
/Users/eifion/.rvm/rubies/ruby-1.9.2-p0/bin/ruby -S bundle exec rspec ./spec/requests/tasks_spec.rb
F..

Failures:

  1) Tasks GET /tasks displays tasks
     Failure/Error: page.should have_content("paint fence")
       expected there to be content "paint fence" in "Task List\ntest js\n\nName"
     # ./spec/requests/tasks_spec.rb:8:in `block (3 levels) in <top (required)>'

Finished in 7.69 seconds
3 examples, 1 failure

La primera especificación falla porque el contenido “paint fence” no aparece en la página. El registro de base de datos que se crea no está disponible para los tests de Selenium, porque nuestras especificaciones usan transacciones en base datos que no son compatibles con Selenium. Para corregir esto podemos modificar el valor de config.use_transactional_fixtures en el fichero spec_helper para que sea falso.

/spec/spec_helper.rb

# If you're not using ActiveRecord, or you'd prefer not to run each of your
# examples within a transaction, remove the following line or assign false
# instead of true.
config.use_transactional_fixtures = false

Con esto los tests pasarán de nuevo, pero esto quiere decir que la base de datos arrastra los registros entre una especificación y otra, lo que no queremos. Para corregir este problema se puede usar una gema llamada database_cleaner para limpiar la base de datos de tests entre una especificación y la siguiente. En la documentación de la gema se explican todas las opciones disponibles.

Para usarla añadiremos una referencia en el fichero Gemfile y ejecutaremos bundle otra vez para instalarla.

/Gemfile

source 'http://rubygems.org'

gem 'rails', '3.0.5'
gem 'sqlite3'
gem 'nifty-generators'

group :development, :test do
  gem 'rspec-rails'
  gem 'capybara', :git => 'git://github.com/jnicklas/capybara.git'
  gem 'launchy'
  gem 'database_cleaner'
end

A continuación modificaremos el fichero spec_helper de nuevo y añadiremos la configuración para borrar la base de datos entre una especificación y otra.

/spec/spec_helper.rb
config.before(:suite) do
  DatabaseCleaner.strategy = :truncation
end

config.before(:each) do
  DatabaseCleaner.start
end

config.after(:each) do
  DatabaseCleaner.clean
end

Si ahora ejecutamos las especificaciones y dejamos que las dos primeras vayan a través de Selenium veremos que todas pasan porque ya no usamos transacciones de base de datos. Parece muy trabajoso, pero una vez que lo tengamos configurado es fácil probar el JavaScript de cualquier especificación simplemente añadiendo la opción :js => true .

En la próxima versión de Capybara (que todavía no hemos visto) hay un nuevo DSL para definir especificaciones con el que podremos usar los métodos feature, background y scenario de forma muy similar a como funciona la gema Steak. Si nos gusta trabajar con este tipo de DSL, conviene saber que Capybara incluirá esta funcionalidad, lo que eliminará la necesidad de usar Steak.