#257 Request Specs and Capybara
- Download:
- source codeProject Files in Zip (131 KB)
- mp4Full Size H.264 Video (22.3 MB)
- m4vSmaller H.264 Video (14.8 MB)
- webmFull Size VP8 Video (39.1 MB)
- ogvFull Size Theora Video (31.3 MB)
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.
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
.
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:
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) <span class="passed">.</span> Finished in 0.18535 seconds <span class="passed">1 example, 0 failures</span>
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.
<% form_for Task.new do |f| %>
Si añadimos esto...
<%= 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 <span class="passed">.</span> Finished in 0.16725 seconds <span class="passed">1 example, 0 failures</span>
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.
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.
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.
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.
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.
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).
<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.
Vamos a probar esta funcionalidad con Capybara. Primero escribimos una nueva especificación:
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 .<span class="failed">F</span>. Failures: 1) Tasks GET /tasks supports js <span class="failed">Failure/Error: page.should have_content("js works")</span> <span class="failed">expected #has_content?("js works") to return true, got false</span> # ./spec/requests/tasks_spec.rb:14:in `block (3 levels) in <top (required)>' Finished in 0.83232 seconds <span class="failed">3 examples, 1 failure</span> 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.
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'
.
# 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.
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
.
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 <span class="failed">F</span>.. Failures: 1) Tasks GET /tasks displays tasks <span class="failed">Failure/Error: page.should have_content("paint fence")</span> <span class="failed">expected there to be content "paint fence" in "Task List\ntest js\n\nName"</span> # ./spec/requests/tasks_spec.rb:8:in `block (3 levels) in <top (required)>' Finished in 7.69 seconds <span class="failed">3 examples, 1 failure</span>
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.
# 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.
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.