homeASCIIcasts

264: Guard 

(view original Railscast)

Other translations: En Ja

Other formats:

Written by Juan Lupión

En el episodio 257 [verlo, leerlo] escribimos una aplicación siguiendo los dictados del desarrollo guiado por tests usando las especificaciones de petición de RSpec. Podemos comprobar que los tests de dicha aplicación pasan en cualquier momento ejecutando rake spec.

$ rake spec
(in /Users/eifion/rails/todo)
/Users/eifion/.rvm/rubies/ruby-1.9.2-p0/bin/ruby -S bundle exec rspec ./spec/models/task_spec.rb ./spec/requests/tasks_spec.rb
...
Finished in 0.92211 seconds
3 examples, 0 failures

Pero se nos puede llegar a hacer un poco tedioso el tener que ejecutar la orden rake cada vez que hacemos un cambio en el código. Si tenemos un conjunto de tests muy grande puede ser que tengamos que esperar mucho tiempo a que los tests pasen, incluso aunque sólo hayamos cambiado uno o dos archivos. Sería útil poder automatizar esto.

Hay varias herramientas para resolver este problema pero en este episodio vamos a echarle un vistazo a Guard, que proporciona una manera de atender las modificaciones que se produzcan sobre un conjunto de archivos y luego ejecutar una acción determinada cuando alguno de los archivos se vea modificado. Igualmente hay un gran número de extensiones disponibles que permiten trabajar con Guard en diferentes entornos incluyendo uno para RSpec que hace exactamente lo que queremos: atender a los cambios en los ficheros y ejecutar las especificaciones cuando dichos cambios se producen. Si en lugar de RSpec estamos usando Test::Unit también existe una extensión, así como para Cucumber, minitest y otros entornos.

Instalación

Probemos Guard en nuestra aplicación con guard-rspec. Empezaremos añadiendo algunas gemas al grupo test del Gemfile. Como estamos desarrollando bajo OS X tenemos que instalar la gema rb-fsevent como prerequisito para Guard así que añadiremos dicha gema sólo si la plataforma es OS X. También añadiremos una referencia a la gema guard-rspec. No hay que añadir guard porque es una dependencia de la gema guard-rspec y se instalará automáticamente.

/Gemfile

source 'http://rubygems.org'

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

group :development, :test do
  gem 'rspec-rails'
  gem 'capybara', :git => 'git://github.com/jnicklas/capybara.git'
  gem 'launchy'
  gem 'database_cleaner'
  gem 'rb-fsevent', :require => false if RUBY_PLATFORM =~ ↵ 
    /darwin/i
  gem 'guard-rspec'
end

Ya podemos ejecutar bundle para instalarlo todo y, una vez que hayamos terminado, ejecutar guard init rspec para configurar Guard.

$ guard init rspec
Writing new Guardfile to /Users/eifion/rails/todo/Guardfile
rspec guard added to Guardfile, feel free to edit it

Si hay algún problema durante la ejecución de esta orden podemos lanzarla con bundle exec. Esta orden generará un fichero llamado Guardfile que por ahora podemos dejar tal y como está (más adelante volveremos a él).

Guard también se integra con el sistema de notificaciones Growl instalando la gema growl, lo que no haremos por ahora.

Ya podemos lanzar la orden guard para lanzar el servidor Guard.

$ guard
Guard is now watching at '/Users/eifion/Desktop/Dropbox/rails/apps_for_asciicasts/ep264/todo'
Guard::RSpec is running, with RSpec 2!
Running all specs
...

Finished in 1.02 seconds
3 examples, 0 failures

Si hacemos un cambio que rompe nuestro código, por ejemplo eliminar una validación del modelo Task, Guard se ejecutará inmediatamente y veremos el test roto.

/app/models/task.rb

class Task < ActiveRecord::Base
  attr_accessible :name
  #validates_presence_of :name
end
Running: spec/models/task_spec.rb
F

Failures:

  1) Task validates name
     Failure/Error: Task.new.should have(1).error_on(:name)
       expected 1 error on :name, got 0
     # ./spec/models/task_spec.rb:5:in `block (2 levels) in <top (required)>'

Finished in 0.04825 seconds
1 example, 1 failure

Tan pronto como volvamos a pone la validación las especificaciones se ejecutarán y pasarán.

Personalización del comportamiento

Es posible modificar el comportamiento de Guard. Por defecto no se ejecutará cuando hagamos un cambio en un fichero de vista, cosa que podemos hacer modificando el Guardfile generado cuando ejecutamos guard por primera vez.

Por defecto el fichero Guardfile tiene el siguiente aspecto:

/Guardfile

# A sample Guardfile
# More info at https://github.com/guard/guard#readme

guard 'rspec', :version => 2 do
  watch(%r{^spec/.+_spec\.rb})
  watch(%r{^lib/(.+)\.rb})                           { |m| "spec/lib/#{m[1]}_spec.rb" }
  watch('spec/spec_helper.rb')                       { "spec" }

  # Rails example
  watch('spec/spec_helper.rb')                       { "spec" }
  watch('config/routes.rb')                          { "spec/routing" }
  watch('app/controllers/application_controller.rb') { "spec/controllers" }
  watch(%r{^spec/.+_spec\.rb})
  watch(%r{^app/(.+)\.rb})                           { |m| "spec/#{m[1]}_spec.rb" }
  watch(%r{^lib/(.+)\.rb})                           { |m| "spec/lib/#{m[1]}_spec.rb" }
  watch(%r{^app/controllers/(.+)_(controller)\.rb})  { |m| ["spec/routing/#{m[1]}_routing_spec.rb",  "spec/#{m[2]}s/#{m[1]}_#{m[2]}_spec.rb", "spec/acceptance/#{m[1]}_spec.rb"] }
end

Cada línea en el fichero es una llamada a un método watch con un bloque que contiene la definición de lo que será ejecutado cuando se produzca un cambio. Por ejemplo si se cambia el fichero de rutas buscará una especificación de rutas. Algunos casos son más complejos y deben recibir una expresión regular. Por ejemplo esta línea controla todos los ficheros Ruby del directorio /lib. El fichero que ha sido modificado queda capturado en la parte de la expresión regular entre paréntesis, y eso es lo que se le pasa al bloque para que se pueda ejecutar la especificación adecuada.

watch(%r{^lib/(.+)\.rb})    { |m| "spec/lib/#{m[1]}_spec.rb" }

Todos los ficheros gestionados por el Guardfile por defecto son ficheros Ruby. Queremos lanzar las especificaciones cuando cambie un archivo de vista, así que añadiremos una nueva línea con watch.

/Guardfile

watch(%r{^app/views/(.+)/}) { |m| "spec/requests/#{m[1]}_spec.rb" }

En esta nueva regla capturamos todo lo que cuelgue debajo del directorio /app/views. Por ejemplo para el archivo /app/views/tasks/index.html.erb la expresión regular devolverá tasks. Esto se le pasa luego al bloque para ejecutar las especificaciones de petición correspondientes.

Podemos hacer la prueba cambiando uno de los archivos de la vista para que rompa un test. Si quitamos el botón de enviar de la vista index de tasks Guard ejecutará las especificaciones y veremos la que falla.

Running: spec/requests/tasks_spec.rb
.F

Failures:

  1) Tasks creates a task with validation error
     Failure/Error: click_button "Add"
     Capybara::ElementNotFound:
       no button with value or id or text 'Add' found
     # ./spec/requests/tasks_spec.rb:13:in `block (2 levels) in <top (required)>'

Finished in 0.82203 seconds
2 examples, 1 failure

Todo volverá a funcionar si volvemos a colocar el botón.

Recarga automática del navegador con Guard

Con Guard es muy fácil ejecutar tests automáticmaente cuando se modifiquen ciertos archivos en una aplicación, pero hay otros usos bastante interesantes. Podemos por ejemplo compilar los archivos CoffeScript y SASS cuando cambien, ejecutar automáticamente la orden bundle cuando se modifique el Gemfile o reiniciar un servidor Passenger o Pow cuando se modifica un fichero de inicialización. El que vamos a ver es guard-livereload que es una gema que recarga automáticamente el navegador cuando se modifican ciertos archivos.

Para utilizarlo añadiremos la referencia en el Gemfile y luego volveremos a lanzar bundle.

/Gemfile

source 'http://rubygems.org'

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

group :development, :test do
  gem 'rspec-rails'
  gem 'capybara', :git => 'git://github.com/jnicklas/capybara.git'
  gem 'launchy'
  gem 'database_cleaner'
  gem 'rb-fsevent', :require => false if RUBY_PLATFORM =~ /darwin/i
  gem 'guard-rspec'
  gem 'guard-livereload'
end

Una vez se haya instalado la gema tenemos que ejecutar guard init livereload para modificar el Guardfile y luego volver a lanzar guard.

A continuación tenemos que instalar una extensión LiveReload para Chrome, Safari o Firefox. La página de LiveReload dispone de enlaces para instarlas. Nosotros instalaremos la versión de Safari.

Con todo esto ya podemos activar la extensión visitando la página principal de nuestra aplicación, hace clic con el botón derecho y escoger “Enable Live Reload”.

Activación de LiveReload

La página se cargará automáticamente en el navegador tan pronto como hagamos un cambio en cualquier archivo de la aplicaicón, por ejemplo la hoja de estilos. Y también si cambiamos el título de la página en un fichero erb, por ejemplo.

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

<% title "Todo List" %>

 

Los cambios en las hojas de estilo se reflejan tan pronto como se guarda el archivo.

Hay algo más de retardo cuando modificamos el fichero erb porque se lanzan las especificaicone antes de que LiveReload recargue la página, y esto sucede así por el orden en que está todo declarado en el Guardfile. Sería más útil que el navegador se actualizase para que podamos chequear la página según se ejecutan los tests. Para ello tan sólo tenemos que modificar los dos bloques de guard en el Guardfile para que la sección de livereload se ejecute antes.

/Guardfile

# A sample Guardfile
# More info at https://github.com/guard/guard#readme

guard 'livereload' do
  watch(%r{app/.+\.(erb|haml)})
  watch(%r{app/helpers/.+\.rb})
  watch(%r{public/.+\.(css|js|html)})
  watch(%r{config/locales/.+\.yml})
end

guard 'rspec', :version => 2 do
  watch(%r{^spec/.+_spec\.rb})
  watch(%r{^lib/(.+)\.rb})                            { |m| "spec/lib/#{m[1]}_spec.rb" }
  watch('spec/spec_helper.rb')                        { "spec" }

  # Rails example
  watch('spec/spec_helper.rb')                        { "spec" }
  watch('config/routes.rb')                           { "spec/routing" }
  watch('app/controllers/application_controller.rb')  { "spec/controllers" }
  watch(%r{^spec/.+_spec\.rb})
  watch(%r{^app/(.+)\.rb})                            { |m| "spec/#{m[1]}_spec.rb" }
  watch(%r{^lib/(.+)\.rb})                            { |m| "spec/lib/#{m[1]}_spec.rb" }
  watch(%r{^app/controllers/(.+)_(controller)\.rb})   { |m| ["spec/routing/#{m[1]}_routing_spec.rb", "spec/#{m[2]}s/#{m[1]}_#{m[2]}_spec.rb", "spec/acceptance/#{m[1]}_spec.rb"] }
  watch(%r{^app/views/(.+)/})                         { |m| "spec/requests/#{m[1]}_spec.rb" }
end

Si ahora hacemos un cambio en un fichero dicho cambio se muestra inmediatamente.