#264 Guard
- Download:
- source codeProject Files in Zip (260 KB)
- mp4Full Size H.264 Video (17 MB)
- m4vSmaller H.264 Video (10.5 MB)
- webmFull Size VP8 Video (24.8 MB)
- ogvFull Size Theora Video (23.6 MB)
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.
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.
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:
# 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
.
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
.
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”.
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.
<% title "Todo List" %>
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.
# 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.