#276 Testing Time & Web Requests
- Download:
- source codeProject Files in Zip (112 KB)
- mp4Full Size H.264 Video (16.4 MB)
- m4vSmaller H.264 Video (9.81 MB)
- webmFull Size VP8 Video (10.8 MB)
- ogvFull Size Theora Video (26 MB)
En el episodio anterior vimos como seguir un proceso de desarrollo basado en tests. Esta forma de trabajo funciona bien por lo general pero hay veces en que las aplicaciones tendrán funcionalidades especialmente difíciles de probar. Veamos dos de estos escenarios en este episodio.
Tests sobre la hora actual
Recordemos que en el otro episodio creábamos un número de pruebas sobre el modelo User
. Las escribimos muy deprisa así que volveremos a una de las especificaciones para inspeccionarla.
it "saves the time the password reset was sent" do user.send_password_reset user.reload.password_reset_sent_at.should be_present end
En este test se comprueba que cuando se envía un mensaje de restablecimiento de contraseña la hora de envío queda guardada en el campo password_reset_sent_at
de la base de datos utilizando la función be_present
de RSpec, que llama al método present?
de Rails que comprueba la existencia de un objeto.
Pero este test está incompleto: comprueba que existe el valor de password_reset_sent_at
pero no que tenga la hora correcta. Establecemos password_reset_sent_at
a Time.zone.now
en el método send_password_reset
del modelo pero la especificación pasaría independientemente del valor que pusiésemos aquí. Idealmente deberíamos comprobar que el valor es correcto con algo parecido a:
it "saves the time the password reset was sent" do user.send_password_reset user.reload.password_reset_sent_at.should eq(Time.zone.now) end
Por desgracia este enfoque no funciona. La hora actual en el instante en que se ejecuta la especificación será ligeramente distinta de la hora cuando actual cuando se ejecute la línea siguiente. Ante un problema como este merece la pena preguntarse si nos compensa meter más complejidad en el test para probar un caso como este, donde parece razonable pensar que basta con comprobar la presencia del valor de tiempo dado que hay muy poca posibilidad de que se ponga la hora incorrecta en la única línea de código que lo hace. Sin embargo hay veces que nos interesa hacer pruebas sobre la hora actual así que investigaremos cómo hacerlo.
Tenemos Guard ejecutándose y podemos ver que falla la especificación. Aunque los valores de tiempo son idénticos hasta el segundo, no son exactamente iguales y esto es suficiente para hacer que falle la especificación.
Failures: 1) User#send_password_reset saves the time the password reset was sent Failure/Error: user.reload.password_reset_sent_at.should eq(Time.zone.now) expected Mon, 25 Jul 2011 20:34:46 UTC +00:00 got Mon, 25 Jul 2011 20:34:46 UTC +00:00 (compared using ==) Diff: # ./spec/models/user_spec.rb:16:in `block (3 levels) in <top (required)>' Finished in 1.95 seconds 9 examples, 1 failure
Para este tipo de problemas podemos usar Timecop, con la que podemosmanipular la hora actual de diferentes maneras, pudiendo incluso congelarla. Esto quiere decir que podemos congelar el tiempo durante el tiempo que se ejecuta el test de forma que el momento en que se genera el valor de tiempo será el mismo que cuando se comprueba que sea el correcto.
Timecop se incorpora en nuestra aplicación añadiéndolo al Gemfile
y ejecutando bundle
. Como sólo lo necesitamos para los tests lo añadiremos al grupo test
.
source 'http://rubygems.org' gem 'rails', '3.1.0.rc4' gem 'sqlite3' # Asset template engines gem 'sass-rails', "~> 3.1.0.rc" gem 'coffee-script' gem 'uglifier' gem 'jquery-rails' gem "rspec-rails", :group => [:test, :development] group :test do gem "factory_girl_rails" gem "capybara" gem "guard-rspec" gem "timecop" end
Es buena idea ir al fichero spec_helper
y añadir una llamada a Timecop.return
en el bloque config.before(:each)
de esta forma los cambios que hagamos con Timecop dejarán de surtir efecto antes de ejecutar la siguiente especificación.
Ya podemos invocar a Timecop.freeze
en cualquier de los tests y el tiempo se parará durante su ejecución. Esto significa que ya podemos comparar los instantes de tiempo:
it "saves the time the password reset was sent" do Timecop.freeze user.send_password_reset user.reload.password_reset_sent_at.should eq(Time.zone.now) end
Cuando Guard ejecute las especificaciones, éstas pasarán correctamente.
Existe otro escenario inesperado que puede hacer que los tests fallen. Por ejemplo si vamos a una conferencia de Ruby en una zona horaria diferente nos podemos encontrar con que de repente los tests relacionados con la fecha fallan. En lugar de recorrer medio mundo de vuelta resultará más barato establecer la zona horaria correspondiente en las especificaciones. Esto se hace estableciendo Time.zone
.
Time.zone = "Paris"
Para establecer temporalmente la zona horaria dentro de un único test podemos llamar a Time.use_zone
.
it "saves the time the password reset was sent" do Timecop.freeze user.send_password_reset Time.use_zone("Paris") do user.reload.password_reset_sent_at.should eq(Time.zone.now) end end
El código dentro del bloque se ejecutará como si estuviésemos en París. Podemos aprovechar esto para comprobar que las especificaciones siguen pasando aunque estemos en una zona horaria diferente.
Siempre que escribamos tests que dependan de la hora actual deberíamos utilizar algo como Timecop para comproba que la fecha se utiliza de forma consistente y que la funcionalidad no cambia si lo hace la zona horaria de nuestro equipo o durante el horario de verano, con lo que además podemos comprobar que nuestra aplicación funcionará en todo el mundo.
Peticiones de web externas
A continuación vamos a ver cómo escribir tests que hacen peticiones a web externas. Esta sección está basada en un problema que se encontró Ryan Bates cuando reescribió la web de Railscasts, específicamente la funcionalidad de mostrar el tamaño de archivo para cada vídeo cuando se pasa el ratón sobre el enlace de descarga.
Como los archivos se encuentran alojados en un servidor externo, hay que lanzar una petición web externa para calcular su tamaño. Veamos cómo se escribieron los tests para esto utilizando el ejemplo de una pequeña aplicación. Tiene un recurso WebRequest y un formulario con un campo de texto que recibe una URL.
Cuando se introduce la URL de un video en la caja de texto y se envía el formulario, se muestra la URL así como el tamaño del archivo que como puede verse tiene cero bytes.
El tamaño de archivo es cero porque todavía no hemos implementado esta funcionalidad. Tenemos un método content_length
en nuestro modelo WebRequest
que está forzado para devolver siempre 0
. Implementemos este método usando TDD.
class WebRequest < ActiveRecord::Base def content_length 0 end end
Tenemos varias gemas con las que escribir tests de peticiones web externas, pero nosotros vamos a usar Fakeweb. Esta gema puede usarse para registrar una URL y definir cuál es la respuesta que se debe devolver cuando se utilice Net::HTTP
para recuperar dicha URL. Por lo tanto, Net::HTTP
no lanzará la petición real sino que devolverá lo que hayamos establecido.
Fakeweb se instala de la forma habitual, añadiéndolo al Gemfile
y ejecutando bundle
. A continuación añadiremos la configuración de Fakeweb haciendo dos cambios en el archivo spec_helper
.
ENV["RAILS_ENV"] ||= 'test' require File.expand_path("../../config/environment", __FILE__) require 'rspec/rails' require 'capybara/rspec' Dir[Rails.root.join("spec/support/**/*.rb")].each {|f| require f} FakeWeb.allow_net_connect = false RSpec.configure do |config| config.mock_with :rspec config.use_transactional_fixtures = true config.include(MailerMacros) config.before(:each) do Timecop.return reset_email FakeWeb.clean_registry end end
En la parte superior del archivo establecemos Fakeweb.allow_net_connect
a false
, lo que hace que las especificaciones no lancen ninguna conexión externa HTTP. Esto es útil porque quiere decir que si hemos dejado otras peticiones web en las especificaciones, éstas no ralentizaran la suite, y Fakeweb nos avisará que la especificación se está conectando a la web. En before(:each)
invocamos a Fakeweb.clean_registry
para que cada especificación comience en el mismo estado.
En la especificación de WebRequest
escribiremos un test que comprueba que se recupera el tamaño del contenido.
require 'spec_helper' describe WebRequest do it "fetches the content length" do FakeWeb.register_uri(:head, "http://example.com", :content_length => 123) WebRequest.new(:url => "http://example.com").content_length.should eq(123) end end
Llamamos a Fakeweb.register_uri
para registrar nuestra URL falsa. El primer argumento del método recibe el tipo de petición que queremos hacer. Del archivo nos interesa recuperar su tamaño a partir de la información del encabezado HTTP, por lo que usamos :head
. Los otros argumentos son la URL y cualquier otra cabecera que queramos, en este caso sólo Content Length
. Luego creamos un nuevo objeto WebRequest
de nuestra aplicación que por dentro invocará esta URL y comprobamos que el valor devuelto por el método content_length
es igual al valor establecido en la cabecera.
Por supuesto, cuando la ejecutamos esta especificación falla porque nuestro método content_length
siempre devuelve 0
. Para que pase tenemos que modificar el método para que devuelta el verdadero valor de tamaño del contenido requerido.
class WebRequest < ActiveRecord::Base def content_length uri = URI.parse(url) response = Net::HTTP.start(uri.host, uri.port) { |http| http.request_head(uri.path) } response["content-length"].to_i end end
Ahora el método invoca a Net::HTTP.start
utilizando la URL recibida en el modelo. El bloque luego recibe llamadas a request_head
para obtener las cabeceras de la URL solicitada. Por último devuelve el valor de la cabecera content-length
.
Por defecto Rails no incluye Net::HTTP
así que tenemos que hacer un require
en nuestra aplicación. Lo haremos en el fichero application.rb
.
require File.expand_path('../boot', __FILE__) require 'net/http' require 'rails/all' # resto del archivo
Con esto pasarán todos nuestros tests y si volvemos a cargar la página de nuestra petición web se mostrará el tamaño correcto.
Fakeweb es una solución muy buena para gestionar peticiones a web externas en nuestros tests.