#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)
Dans l'épisode précédent, nous avons montré un workflow de développement dirigé par les tests. En majeure partie, le modèle utilisé fonctionne bien mais il peut arriver que certaines fonctionnalités de notre application soient difficiles à tester. Nous allons voir deux scénarios de ce type dans cet épisode.
Tester le temps actuel
La dernière fois, nous avons créé un certain nombre de specs pour tester le modèle User
. Nous les avons écrites assez rapidement et nous allons maintenant revenir sur l'une d'elles et nous attarder quelque peu dessus.
it "saves the time the password reset was sent" do user.send_password_reset user.reload.password_reset_sent_at.should be_present end
Cette spec vérifie que, lorsqu'une demande de réinitialisation de mot de passe est envoyée, le moment de l'envoi est stocké dans le champ password_reset_sent_at
. Ceci est testé grâce au matcher be_present
de RSpec. Ce matcher appelle une méthode, fournie par Rails, nommée present?
qui vérifie l'existence d'un objet.
Cette spec est incomplète : elle vérifie qu'une valeur existe pour password_reset_sent_at
mais ne s'assure pas que cette valeur est le temps actuel. Nous renseignons bien password_reset_sent_at
à Time.zone.now
, dans la méthode send_password_reset
du modèle User
, mais la spec passerait, peu importe la valeur renseignée. Idéalement, nous devrions tester que la valeur est la date actuelle en écrivant quelque chose comme ceci :
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
Malheureusement cela ne peut pas fonctionner. Le temps actuel, au moment du test, sera légèrement après le moment ou le code testé est exécuté. Lorsque l'on est face à un problème comme celui-ci, cela vaut la peine de se poser la question de l'utilité d'ajouter une certaine complexité aux tests afin qu'ils soient complets. Dans de nombreux cas, comme celui-ci, il est suffisant de tester que la valeur du timestamp existe. Il y a peu de chances qu'un bug ait lieu dans une ligne de code qui renseigne le temps mais il peut arriver que nous ayons besoin de tester le temps actuel. Regardons donc comment nous pourrions le faire dans les specs.
Guard tourne et nous pouvons voir que notre spec échoue. Bien que le timestamp soient très proches en terme de secondes, ils ne sont pas identiques et cela suffit à causer l'échec de la spec.
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
Nous pouvons gérer ce genre de problèmes en utilisant une gem nommée Timecop. Nous pouvons l'utiliser pour manipuler le temps actuel de plusieurs façons, y compris en le gelant. Cela signifie que nous pouvons geler le temps actuel durant le fonctionnement de la spec afin que le moment auquel le timestamp est renseigné soit exactement le même que lors du test.
Nous pouvons ajouter Timecop à notre application en l'ajoutant dans le Gemfile
et en lançant bundle
. Puisque nous en avons seulement besoin pour nos tests, nous le plaçons dans le groupe 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
Il est maintenant temps d'aller dans le fichier spec_helper.rb
et d'ajouter un appel à Timecop.return
dans le bloc config.before(:each)
. Cela assure le fait que tout changement effectué avec Timecop est annulé avant le lancement de chaque spec.
Nous pouvons appeler Timecop.freeze
dans n'importe quelle spec et geler le temps durant l'exécution de cette dernière. Cela signifie que nous pouvons comparer les deux timestamps.
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
À présent, lorsque Guard lance les specs, elles passent toutes.
Il y a un autre scénario inattendu qui peut causer l'échec des tests. Si vous assistez à une conférence sur Ruby dans un autre fuseau horaire, vous pouvez vous retrouver avec un échec de vos tests relatifs au temps. Au lieu de parcourir la moitié du monde, il est bien moins coûteux de régler le fuseau horaire dans les specs. Cela se fait en réglant Time.zone
:
Time.zone = "Paris"
Pour régler temporairement le fuseau, pour une seule spec, nous pouvons appeler 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
Le code à l'intérieur du bloc sera lancé comme si nous étions à Paris. Nous pouvons utiliser cela pour tester que nos specs passent toujours, même si nous somme dans un autre fuseau horaire.
Chaque fois que nous testons de temps actuel, nous devrions utiliser quelque chose comme Timecop de façon à ce que le temps puisse être manié de manière consistante et ne change pas lors d'un changement de fuseau horaire ou d'un passage à l'heure d'été. Nous devrions également l'utiliser pour tester notre code dans différents fuseaux horaire afin de vérifier que notre application fonctionne mondialement.
Tester les requêtes web externes
Nous allons jeter un oeil au test des requêtes web externes. Cette section est basée sur un problème que Ryan Bates a rencontré lors de la réécriture du site de Railscasts, en particulier la fonctionnalité qui affiche la taille des fichiers pour chaque format vidéo lorsque vous passez sur le lien télécharger.
La taille du fichier est récupérée depuis un serveur web externe puisque les fichiers média sont stockés sur un serveur à part. Cela signifie qu'une requête web externe est effectuée et nous allons maintenant montrer comment cela a été testé. Nous allons le faire grâce à une petite application d'exemple. Elle a une ressource WebRequest et un formulaire contenant un champ texte qui attend une URL.
Lorsque nous saisissons l'URL d'une vidéo dans le champs texte et soumettons le formulaire, l'URL est affichée ainsi que la taille du fichier indiquant zéro octet.
La taille du fichier est zéro puisque nous n'avons pas encore implémenté cette fonctionnalité. Nous avons, dans notre modèle WebRequest
, une méthode content_length
mais cette méthode est codée en dur pour retourner 0
. Nous allons maintenant l'implémenter en utilisant le TDD (développement dirigé par les tests).
class WebRequest < ActiveRecord::Base def content_length 0 end end
Il existe un certain nombre de gems aidant au test des requêtes web externes mais nous allons utiliser Fakeweb. Cette gem peut être utilisée pour enregistrer une URI et définir ce que sa réponse doit être. Lorsque nous utilisons Net::HTTP
pour récupérer cette URI, elle va, à la place, retourner cette réponse plutôt que faire une requête externe.
Fakeweb est installé, comme d'habitude, en l'ajoutant dans le Gemfile
et en appelant bundle
. Nous allons ensuite ajouter une configuration pour Fakeweb en effectuant deux modifications dans le fichier spec_helper.rb
.
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
Près du début du fichier, nous renseignons Fakeweb.allow_net_connect
à false
ce qui va empêcher les specs d'effectuer des connexion HTTP externes. Cela permet de faire en sorte que, si nous avons oublié des requêtes externes dans nos specs, elles ne ralentiront pas la totalité de la suite de tests et Fakeweb nous fera savoir que les specs tentent de se connecter au web. Dans before(:each)
, nous appelons Fakeweb.clean_registry
afin que chaque spec démarre dans le même état.
Dans les specs pour WebRequest
, nous allons écrire une spec qui va vérifier que le contenu est récupéré.
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
Nous appelons FakeWeb.register_uri
pour enregistrer une fausse URL. Le premier argument de cette méthode est le type de requête que nous voulons faire, nous pouvons obtenir la taille du fichier depuis les informations d'en-tête, nous utilisons donc :head
. Les autres arguments sont l'URL et les en-têtes de notre choix. Dans notre cas, la longueur du contenu (Content Length). Nous créons ensuite un nouvel objet WebRequest
qui appelle cette URL et vérifions que la valeur retournée par la méthode content_length
est égale à la valeur saisie dans l'en-tête.
Bien sûr, lorsque nous lançons la spec, elle échoue puisque notre méthode content_length
retourne toujours 0
. Pour faire passer la spec, nous devons modifier cette méthode afin qu'elle retourne la vraie valeur du content length pour le fichier demandé.
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
La méthode appelle maintenant Net::HTTP.start
en utilisant l'URL passée au modèle. Le bloc passé appelle request_head
pou récupérer l'en-tête de la réponse. Enfin, elle retourne la valeur de l'en-tête content-length
.
Rails n'inclue pas Net::HTTP
par défaut, nous devons donc ajouter un require
dans notre application. Nous allons de le faire dans le fichier application.rb
.
require File.expand_path('../boot', __FILE__) require 'net/http' require 'rails/all' # rest of file
Toutes nos specs passent à présent et, lorsque nous rechargeons la page de notre requête, la bonne taille de fichier est affichée.
Si jamais vous avez besoin de gérer des requêtes web externes dans vos tests, Fakeweb est une très bonne solution.