Introduction à Capybara dans Rails 3.1

Publié le 13 décembre 2011 par Nicolas Zermati | outils

Cet article est publié sous licence CC BY-NC-SA

Avez vous remarqué que le test logiciel est une des tendances les plus à la mode ces dernières années ? Je ne dis pas que le test logiciel est un concept récent, au contraire. Cependant des pratiques émergentes comme l’XP, le TDD ou le BDD ne cesse de prendre de l’ampleur et de révolutionner la manière de produire des logiciels. Chacune de ces pratiques apportent de nouvelles philosophies et de nouvelles techniques ; par exemple l’Intégration Continue, le pair-programming, RSpec ou Cucumber.

Ruby et sa communauté accompagnent ces évolutions, les deux références précédentes en témoignent. Le projet Ruby on Rails possède lui aussi la volonté d’intégrer ces nouvelles pratiques. Une autre tendance se dessine de son coté dans le monde du web : répartir les fonctionnalités d’une application sur le client et le serveur grâce à du Javascript, aux web-sockets, etc.

N’échappant pas à ces influences, je vais tenter aujourd’hui de vous présenter les tests dits d’intégration mis en place par Rails ainsi que l’utilisation de Capybara, une bibliothèque externe permettant de simuler des actions utilisateur, dans le but de réaliser des test dits de validation (« Acceptance testing »). L’intérêt de Capybara : gérer les scénarios complexes dans lesquels une partie de la logique de l’application est assuré par du Javascript ; nous y reviendrons par la suite.

Les tests d’intégration et les tests de validation

Rails distingue plusieurs type de tests : unitaires, fonctionnels, d’intégration et de performance. Chacun de ces types de test répond à un besoin bien précis : les tests unitaires servent à tester les modèles, les tests fonctionnels à tester les contrôlleurs ainsi que leurs vues et les tests d’intégration à tester les interactions entre différents contrôlleurs.

Les tests de validation sont les plus proches des exigences des clients, ils décrivent les fonctionnalités telle qu’elles sont perçu par les utilisateurs finaux. En effet, un client dira : « Lorsque j’arrive sur la page de l’évènement, je voudrais cliquer sur un lien et voir la liste des invités prévus. ». Voila, le client vient de formuler un test de validation. On utilisera ce test pour valider la concordance entre ce que le client attend et ce que l’application fera.

Les tests d’intégrations simulent la navigation d’un utilisateur. Dans Rails, la différence entre ces deux types de test demeure assez floue. Le temps de cet article, ces tests seront confondus.

Décrire un test

Maintenant que nous avons une idée des tests dont nous parlons, on va commencer par créer un squelette grâce à la commande suivante.

$ rails g integration_test list_guest

Cette commande va créer le fichier test/integration/list_guest_test.rb, quasiment vide :

$ rails g integration_test list_guest

Dans cet exemple, le test sera de vérifier que lorsqu’on clique sur le lien « Show guests » le nombre d’invités affichés est correct. Pour cela le cheminement de l’utilisateur va être le suivant : arriver directement sur la page d’un évènement, cliquer sur le fameux lien et voir que la liste des invités comprends le nombre d’invités attendus. Ceci est un premier test et sera notre fil conducteur.

Un test d’intégration classique va nous permettre d’effectuer des requêtes et d’accéder aux réponses reçues à travers une Session qui est automatiquement ouverte pour le test. C’est donc directement à travers de la Session que nous allons décrire notre test de la manière suivante.

require 'test_helper'

class ListGuestTest < ActionDispatch::IntegrationTest
  fixtures :all

  # test "the truth" do
  #   assert true
  # end
end

Dans l’exemple précédent, les deux fonctions guests_path et guests_count ne sont pas implantées. Il existe différentes manières de le faire. Dans l’optique d’un test de validation il faudrait adopter le point de vue de l’utilisateur en traversant l’ensemble de la stack applicative. Alors que, dans l’optique du test d’intégration, on va simplement s’assurer que les controlleurs possèdes les bonnes données.

Qu’on adopte une approche ou bien une autre, on peut tout à fait utiliser les outils de Ruby et de Rails. Par exemple on peut compter le nombre d’évènements soit grâce à une expression régulière appliquée sur le corps de la réponse soit en utilisant directement le dernier controlleur utilisé (voir Session::controller). Remarquez toutefois que les expressions régulières pour analyser du XML et en extraire des informations, on a vu mieux…

Déporter de la logique vers le client… et la tester !

Jusque là, vous l’aurez deviné les modèles sont assez simples : Event et Guest. La relation N-N entre les deux tombe sous le sens elle aussi. Autre chose moins évidente peut être : l’affichage de la liste des invités depuis la vue évènement nécessite de suivre un lien et de charger une nouvelle page (c’est ce que fait le get guests_path). On peut imaginer les routes associées : /events/:id et /events/:event_id/guests.

Admettons maintenant que le client souhaite afficher les invités sur la même page. Un solution est d’utiliser du code Javascript ou Coffescript pour charger la liste des invités et l’afficher dans la page de l’évènement. Et bien là, on sort du prérimètre des tests d’intégration de Rails. On cherche à tester un comportement dynamique controllé non plus par la logique des controlleurs de l’application mais par la logique coté client en Javascript.

Le but des tests de validation est d’adopter le point de vue de l’utilisateur final, peu importe ou se trouve le code. Rails ne propose pas encore de solution intégrée pour tester ces aspects dynamiques. Ce type de répartition de la logique de l’application est pourtant de plus en plus présente. Pas de panique pour autant, ces comportements peuvent tout de même être testés ; soit par du test unitaire Javascript accompagnés d’une confiance aveugle dans son intégration au reste de l’application soit par des solutions comme Capybara.

Voila ce prélude introduit la problématique à laquelle Capybara propose une solution : « Comment décrire des tests de validation permettant de simuler le comportement d’un utilisateur ? ». Pour répondre à cette question, Capybara fournit un DSL destiné à accéder au corps de la requête via des chemins XPath ou CSS. Ce qui est le plus intéressant pour notre exemple c’est cependant l’intégration d’un driver Javascript qui va permettre d’exécuter du code client. Selenium est un des drivers disponible et est inclu dans les dépendances de Capybara. Selenium va charger un navigateur en entier, d’autres drivers dits headless ne feront pas de rendu graphique, gagnant ainsi en performance. Dans la suite de cet article j’utiliserais Selenium.

Ci dessous, un aperçu de ce que va donner notre test avec Capybara. Ce test n’est pas parfait : il contient trois assertions qui pourraient être séparées en sous-tests et laisse probablement passer quelques cas un peu tordus. L’objectif est de vous donner un premier contact avec Capybara.

require 'test_helper'

class ListGuestTest < ActionDispatch::IntegrationTest
  fixtures :all
  include Capybara::DSL

  test "# visible guests should be equal to the total # of guests" do
    @event = Event.find(:first, :order => 'random()')
    visit event_path(@event)

    # Au chargement, les invitès ne doivent pas figurer dans la page
    assert_equal 0, all('.guest').size

    click_on "Show guests"

    # On doit voir le bon nombre d'invités une fois le lien cliqué
    assert has_css?('.guest', :count => @event.guests.size)
  end
end

On voit apparaître trois mots clés du DSL : visit, click_on et has_css?. Les deux premier permettent de naviguer et le dernier permet de tester les noeuds du DOM de la page courante. Remarquez aussi que les helpers de tout à l’heure : guests_path et guests_count ont disparus grâce à ces mots clés.

Le test attends une page qui dans un premier temps ne contient pas d’élément répondant au selecteur CSS .guest. Ensuite on va chercher un lien ou un bouton nommé « Show guests » afin de simuler un clique sur ce dernier. Si aucun lien portant ce nom n’est trouvé alors une exception sera levée. Pour finir, on vérifie que le DOM contient bien le nombre d’invités attentu en comptant le nombre d’éléments .guest.

Ajouter Capybara à votre projet

Pour ajouter Capybara à votre projet : ajoutez d’abord la dépendance à votre Gemfile. On ajoute y également launchy, ce dernier gem permet de lancer un navigateur pour debugger les tests grâce à la commande Capybara save_and_open_page (entre autres).

group :test do
  gem 'capybara'
  gem 'launchy'
end

Ensuite, ajoutez, le Mixin Capybara::DSL dans vos classes de test comme dans l’exemple précédent, soit plus directement dans votre test_helper.rb en l’ajoutant dans la classe ActionDispatch::IntegrationTest. Dans le deux cas, un require 'capybara/rails' sera nécéssaire.

ENV["RAILS_ENV"] = "test"
require File.expand_path('../../config/environment', __FILE__)
require 'rails/test_help'

require 'capybara/rails'

# Choix du driver par défaut : selenium pour le Javascript
Capybara.default_driver = :selenium


class ActionDispatch::IntegrationTest
  # Intégration de Capybara dans tout les tests d'intégration.
  include Capybara::DSL
end

class ActiveSupport::TestCase
  # Setup all fixtures in test/fixtures/*.(yml|csv) for all tests in alphabetical order.
  #
  # Note: You'll currently still have to declare fixtures explicitly in integration tests
  # -- they do not yet inherit this setting
  fixtures :all

  # Add more helper methods to be used by all tests here...
end

On pourrait maintenant penser que c’est installé. Tant que vous n’utiliserez pas de Javascript, vous n’aurez pas de problèmes. Cependant, par défaut Capybara utilise Rack::Test comme driver et ce dernier n’est pas en mesure d’exécuter le code Javascript client. Comme je le disais plus tôt, Capybara possède une dépendance à selenium-webdriver. Indiquer l’utilisation de Selenium, peut se faire soit localement soit globalement. Dans mon cas je le fait globalement dans le fichier test_helper.rb ci dessus. Il est possible de changer le driver selon les tests pour gagner en performance.

Détails utiles

Exécuter du Javascript dans ses tests

Ma première utilisation de Capybara fut pour tester le comportement d’une balise <audio> dans du HTML. Pour cela il m’a fallut injecter mon propre Javascript dans la page afin d’intercepter l’appel de certaines fonctions comme par exemple play(). Il est possible de faire ça grâce à la commande execute_script offerte par Capybara.

Cette commande permet donc d’avoir accès aux bibliothèques javascript présentes comme jQuery. Par contre cette commande ne fonctionnera qu’avec des drivers supportant Javascript, excluant donc Rack::Test.

Puisqu’il arrive d’être obligé d’utiliser du Javascript pour tester certains aspects d’une page, il faut un moyen de faire remonter les informations depuis le Javascript jusqu’au code Ruby du test. La fonction execute_script doit permet d’évaluer des expressions simples. N’ayant pas eu beaucoup de succès avec son utilisation, je préfère vous proposer une autre solution.

Cette méthode consiste à ajouter dynamiquement une balise <meta> dans le DOM puis d’écrire les informations dans ses attributs depuis du code Javascript. Ensuite, on utilise Capybara pour récupérer les attributs de la balises qu’on aura modifié. On peut imaginer écrire du JSON pour passer des structures complexes.

Cette approche permet d’utiliser, dans ses tests, les possibilités offertes par le Javascript. Je suis cependant preneur s’il existe d’autres moyens de communication entre le driver Javascript et les tests Ruby.

Utiliser un HTTP_USER_AGENT différent

Parfois il se peut que vous souhaitiez forcer l’USER_AGENT utilisé pour les tests. Cet article donne un exemple permettant de créer un nouveau driver à partir de Selenium. Dans l’exemple on force le header à iPhone.

Capybara.register_driver :iphone do |app|
  # Surcharger l'HTTP_USER_AGENT globalement en créant un nouveau driver
  require 'selenium/webdriver'
  profile = Selenium::WebDriver::Firefox::Profile.new
  profile['general.useragent.override'] = "iPhone"

  Capybara::Driver::Selenium.new(app, :profile => profile)
end

Capybara.default_driver = :iphone

Autres drivers

D’autres drivers son disponibles, les drivers headless sont particulièrement intéressant pour le coté performance comme je le disais. J’ai également utilisé capybara-webkit.

Ce driver se base sur le webkit de QT4, il faudra donc disposer de cette dépendance pour la compilation du gem. D’autre part mon expérience avec la balise <audio> donnaient lieu à des crashs du webkit, Selenium m’a semblé plus robuste bien que beaucoup plus lent lors de l’exécution des tests.

Code de ce billet

Le code associé à ce billet est disponible sur Github. Lancez rake test:integration pour lancer le test (après un bundle, un rake db:create et un rake db:migrate). C’est surtout intéressant pour voir les fixtures, les routes et les modèles de l’application que je n’ai pas détaillé ici.

La suite

Il reste un certains nombre de points que je n’ai pas abordés dans cet article. Ils sont toutefois non négligeables comme par exemple les Transactional fixtures ou la sélection du driver en fonction des tests. La documentation n’est pas mal faite et il y a une communauté d’utilisateurs qui ont déjà écrit sur les questions les plus communes.

Si le domaine des tests vous intéresse vous trouverez certainement l’article de Nicolas sur minitest à votre gôut. Son article est complémentaire à celui ci puisqu’il traite de l’outil minitest d’une manière indépendante aux types de tests.

L’équipe Synbioz.

Libres d’être ensemble.