Bienvenue sur cette nouvelle page de mon blog. Elle va me permettre de partager les bonnes pratiques d’intégration continue que j’ai pu expériencer en Java, en les intégrant au framework Ruby on Rails.
Tout d’abord, pour ceux qui ne connaissent pas l’intégration continue, nous allons définir ce que c’est : l’intégration continue est le fait de construire en continu un projet de manière automatisée, tout en pouvant effectuer des tâches annexes, comme exécuter des tests unitaires, des revues de qualité de code, etc… Les plateformes d’intégration continue les plus connues et utilisées sont Apache Continuum et CruiseControl. Les constructions de projets avec ces outils fonctionnent avec Maven, ou Ant par exemple.
L’intérêt de cette page est donc d’obtenir une plateforme complète d’intégration continue fonctionnelle pour Ruby on Rails. Ce framework est encore jeune, mais pourquoi ne pourrait-il pas en profiter, alors que tous les outils sont disponibles ?
1 – Commencer par la base : le serveur d’intégration continue CruiseControl.rb
Ce serveur est même plus que ça, c’est aussi une application Rails à part entière. Vous ne serez donc pas perdus pour savoir comment le personnaliser.
Pour commencer, téléchargez la dernière version sur le site web de l’éditeur.
Maintenant que vous avez CruiseControl de dézippé dans un répertoire, vous devez savoir deux choses sur votre projet Rails :
- Vous devez le publier sur un référentiel Subversion (seul ce type de référentiel est géré)
- Pour que votre projet puisse être construit correctement, vous devez avoir configuré vos bases de données et créé au moins un fichier de migration Rails
Une fois que ces requis sont remplis, ajoutez votre projet Rails dans CruiseControl. Positionnez-vous à la racine du répertoire de CruiseControl, et tapez :
./cruise add [votre_nom_de_projet] --url [URL acces svn] [[--username [nom user svn] --password [password user svn] ]]
Une fois que vous avez fait cela, vous pouvez démarrer CruiseControl :
./cruise start
Ouvrez votre navigateur à l’URL http://localhost:3333 : votre projet est en train d’être construit pour la première fois. Si la construction n’aboutit pas, allez en voir le détail pour effectuer les corrections nécessaires.
Avant de finir, il est important de voir que votre construction de projet peut être personnalisée : il suffit de modifier le fichier projects/[votre projet]/cruise_config.rb afin de l’adapter à vos besoins
2 – Les tests et l’intégration continue
Dans ce chapitre, nous allons nous attaquer au vaste sujet des tests dans le développement, au sens du framework Ruby On Rails. Nous allons voir également que l’exécution des tests est déjà préintégrée à l’intégration continue sans que l’on ait à faire quoi que ce soit, car ils sont compris dans les tâches de construction de base (via la commande ‘rake test’ par exemple).
2.1) Les tests unitaires
Les tests unitaires font partie intégrante du framework Ruby On Rails, car ils sont intimement liés au modèles. Par exemple, imaginons que nous voulons générer un modèle pour la gestion d’utilisateurs. Nous faisons alors :
./script/generate model user
En voici la sortie :
exists app/models/
exists test/unit/
exists test/fixtures/
create app/models/user.rb
create test/unit/user_test.rb
create test/fixtures/users.yml
exists db/migrate
create db/migrate/001_create_users.rb
Prenons en considération les fichiers importants : les fichiers test/unit/user_test.rb et test/fixtures/users.yml. Respectivement, ce sont un fichier de tests unitaires, et un fichier d’alimentation de données au format YAML qui sera chargé avant le lancement des tests unitaires.
Un autre fichier est facultatif mais intéressant : le fichier de migration de base de données (db/migrate/001_create_users.rb). Editons le pour créer la table users :
class CreateUser false do |t|
t.column :login, :string, :limit => 20
t.column :nom, :string, :limit => 255
t.column :prenom, :string, :limit => 255
t.column :email, :string, :limit => 255
t.column :password, :string, :limit => 32
end
end
def self.down
# On détruit la table users
drop_table :users
end
end
Pour créer la table en base de données, nous devons lancer la tâche de migration :
rake db:migrate
Maintenant, alimentons nos données de tests en éditant le fichier test/fixtures/users.yml. Attention, pour le passage des cas de tests, si vous utilisez ce style de fichiers, les données seront chargées dans votre base de données. Autant le faire alors dans l’environnement de test Rails.
vdu: id: 1 login: collaborateurvdu nom: VDU prenom: Collaborateur email: cvdu@rubyonrails.org password: 1234567890ABCDEFGHIJKLMNOPQRSTUV other: id: 2 login: ayafly nom: FLY prenom: Abdel Yves Akhim email: abdelyvesakhimfly@rubyonrails.org password: 1234567890ABCDEFGHIJKLMNOPQRSTUV
Voilà ! Nous sommes maintenant armés pour réaliser des cas de tests unitaires efficaces. Editons le fichier test/unit/user_test.rb :
require File.dirname(__FILE__) + '/../test_helper'
class UserTest 'test', :nom => 'TEST', :prenom => 'Test', :email => 'test@test.com', :password => 'test'}
user = User.create params
user.save
user_get = User.find_by_email 'test@test.com'
assert_not_nil user_get
user_get.destroy
deleted_user_get = User.find_by_email 'test@test.com'
assert_nil deleted_user_get
end
end
Lancez maintenant les commandes suivantes pour exécuter vos tests unitaires :
export RAILS_ENV=test rake test:units
Bien entendu cette présentation des tests unitaires est assez succinte; vous pourrez trouver un excellent guide complet sur le site de Ruby On Rails : A guide to testing the Rails
2.2) Les tests de contrôleurs
Les contrôleurs étant les points d’accès à vos applications Rails, il est plus que conseillé de les tester. On peut imaginer par exemple dans un cas idéal réaliser un cas de test pour chaque scénario d’exécution d’une fonctionnalité d’un système. Bon OK, souvent, vu le temps de création de tests auquel on a droit, on en fait pour la plupart le strict minimum (–note aigrie d’informaticien, très rare–).
Dans cette section, nous n’allons pas tout voir, car les tests de contrôleurs peuvent aller très loin (tests de contruction d’URLs, de présence de tags HTML en réponse, d’uploads…). Nous allons uniquement couvrir les cas les plus courants, c’est à dire les accès directs (get) ou les tests sur formulaires (la plupart du temps, des post).
Commençons par présenter une classe de test de contrôleur vierge (généré par Rails lors de la création d’une HomeController). Selon la norme Rails, il se situe dans test/functional/home_controller_test.rb :
require File.dirname(__FILE__) + '/../test_helper'
require 'home_controller'
# Re-raise errors caught by the controller.
class HomeController; def rescue_action(e) raise e end; end
class HomeControllerTest < Test::Unit::TestCase
def setup
@controller = HomeController.new
@request = ActionController::TestRequest.new
@response = ActionController::TestResponse.new
end
def test_truth
# Mettre ici votre code de test
end
end
Débutons le plus simplement possible : nous allons tester que le point d’entrée de l’application (contrôleur HomeController, action index) répond correctement. Renommons la méthode ‘test_truth’ en ‘test_index’, et ajoutons-y le code nécessaire :
def test_index get :index assert_response :success # Equivalent à assert_response 200 (voir http://manuals.rubyonrails.com/read/chapter/28#page234) end
Rajoutons-y un peu de piment : vous avez une application qui affiche une fiche client, vous voulez tester le point d’entrée en lui passant un vrai identifiant de client, puis un faux :
require File.dirname(__FILE__) + '/../test_helper'
require 'customer_controller'
# Re-raise errors caught by the controller.
class CustomerController; def rescue_action(e) raise e end; end
class CustomerControllerTest "12"} # Client existant
assert_response :success
assert_not_nil flash[:customer]
get :detail, {'id' => "XBR22"} # Client inexistant
assert_response :redirect
# Oui, nous testons ici la redirection, car vous gérez les exceptions,
# et vous redirigez sur une page commune d'erreur au besoin
# Nous pourrions tester aussi :
assert_nil flash[:customer]
end
end
Prémices d’industrialisation du processus :
Il est très contraignant de devoir écrire des tests pour tous les points d’accès directs d’un contrôleur, alors que l’on pourrait très bien faire :
require File.dirname(__FILE__) + '/../test_helper'
require 'home_controller'
# Re-raise errors caught by the controller.
class HomeController; def rescue_action(e) raise e end; end
class HomeControllerTest < Test::Unit::TestCase
def setup
@controller = HomeController.new
@request = ActionController::TestRequest.new
@response = ActionController::TestResponse.new
@urls = YAML::load(File.read("test/functional/urls.yml"))
end
def test_access_points
@urls.each do |url|
get url[0]
assert_response url[1]
end
end
end
Dans la classe de test ci-dessus, on automatise l’exécution des cas de tests en externalisant les URLs à tester dans un fichier YAML. Voici le contenu de ce fichier :
--- - - index - 200 - - register - 200 - - forgot_password - 200
Ce fichier nous permet de récupérer un tableau à double dimension contenant le nom de l’action à tester, et le code HTTP de retour attendu. Comme vous pouvez le voir, c’est assez succint, cet automatisme pourrait largement être amélioré.
Attaquons-nous maintenant aux tests des formulaires de saisie : vous vous dites peut être que cela est compliqué; en fait, cela est presque aussi simple que ce que nous avons fait jusqu’à présent. Imaginons que nous avons un formulaire de connexion à une application de la forme :
"home", :action => "login" do %>
Pour tester ce formulaire, rajoutons une nouvelle méthode de test à notre classe de test du HomeController. Le comportement de la connexion est : l’utilisateur connecté est positionné en session, et on le redirige vers son tableau de bord :
def test_login # Ci-dessous, les clé utilisées dans le pseudo-fichier YAML # correspondent à celles du formulaire de saisie post :login, YAML.load(< 'home', :action => 'board' assert_not_nil session["user_session"] end
Enfin, pour lancer vos tests :
export RAILS_ENV=test rake test:functionals
Vous voici maintenant bien armés pour tester vos contrôleurs Rails. Si vous souhaitez aller plus loin, vous pouvez lire la page correspondante du fameux guide Rails de tests.
2.3) TU et TNR avec Selenium on Rails
Maintenant que nous avons une base solide en termes de tests unitaires pour Rails, nous allons étudier ici un outil puissant servant à automatiser des campagnes de tests unitaires mais aussi et surtout de tests de non-régression, Selenium. A la base, Selenium est un outil fait pour toutes les applications de type Internet. Utilisateurs de Rails, sachez que vous avez été choyés, car les créateurs de Selenium vous ont créé un plugin qui permet d’intégrer Selenium sans effort à n’importe quel projet Rails. Encore mieux, il est possible de créer des campagnes de tests avec Selenium IDE (plugin pour Firefox), et d’exporter ces campagnes en tests Ruby. L’automatisation ici atteint son comble.
Un seul petit bémol à tout ce mécanisme. En étudiant ce plugin, j’ai pu observer un petit problème : si on intègre la tache Rake de lancement des tests Selenium à CruiseControl, les campagnes de tests sont exécutées, mais à la fin, Firefox ne se ferme pas et ne rend pas la main au thread Ruby, donc l’ensemble du build CruiseControl échoue (testé uniquement sous Linux, néanmoins). C’est la seule limite de ce système, il vaut mieux lancer ces campagnes de tests à la main et en observer les résultats.
Voyons maintenant comment installer et utiliser ce plugin. Pour commencer, lancer la commande :
ruby script/plugin install http://svn.openqa.org/svn/selenium-on-rails/selenium-on-rails
Si vous êtes sous Windows, il faut installer un gem supplémentaire :
gem install win32-open3
Pour vérifier que tout s’est bien déroulé, vous devez tester le plugin :
cd vendor/plugins/selenium-on-rails/ rake
Si tout se passe correctement, vous pouvez continuer, sinon je vous recommande chaudement d’aller consulter la page Selenium on Rails.
Créons un cas simple, par exemple pour se logger à une application Rails. Lancez Selenium IDE, puis tapez dans le navigateur l’URL de votre application. Par exemple un test qui consiste à se connecter puis se déconnecter de l’application peut donner comme résultat :

Quand vous avez fini votre test, dans Selenium IDE, faites : Fichier -> Exporter le Test sous… -> Ruby – Selenium RC. Enregistrez le fichier avec l’extension .rsel dans votre répertoire test/selenium/.
La dernière étape consiste à enlever les choses inutiles dans notre fichier de test. Prenons par exemple :
require "selenium"
require "test/unit"
class login2 < Test::Unit::TestCase
def setup
@verification_errors = []
if $selenium
@selenium = $selenium
else
@selenium = Selenium::SeleneseInterpreter.new("localhost", 4444, "*firefox", "http://localhost:4444", 10000);
@selenium.start
end
@selenium.set_context("test_login2", "info")
end
def teardown
@selenium.stop unless $selenium
assert_equal [], @verification_errors
end
def test_login2
@selenium.open "http://localhost:3000/"
@selenium.type "login_login", "xxxxxxx"
@selenium.type "login_password", "xxxxxxxx"
@selenium.click "link=Connexion"
@selenium.wait_for_page_to_load "30000"
assert_equal "", @selenium.get_title
@selenium.click "link=Déconnexion"
@selenium.wait_for_page_to_load "30000"
assert_equal "", @selenium.get_title
end
end
Le fichier final ne devra contenir que :
open "http://localhost:3000/" type "login_login", "xxxxxxx" type "login_password", "xxxxxxxx" click "link=Connexion" wait_for_page_to_load "30000" assert_title "votre titre de test" click "link=Déconnexion" wait_for_page_to_load "30000" assert_title "votre titre de test"
Il faut donc réadapter le fichier quelque peu. Pour plus d’informations sur la syntaxe des commandes, vous pouvez consulter SeleniumOnRails::TestBuilder.
Vous n’avez plus qu’à tester ! Lançez votre serveur :
ruby script/server -e test
Puis pointez votre navigateur sur l’URL : http://localhost:3000/selenium. Voilà, plus qu’à lancer vos tests !
Pour finir, si vous voulez lancer vos tests automatiquement, vous pouvez le faire. Tout d’abord, modifiez le fichier vendor/plugins/selenium-on-rails/config.yml à votre convenance. Ensuite, lancez les tests en tapant :
rake test:acceptance
Votre serveur doit être démarré pour que cela fonctionne.
3 – Intégration continue et qualité du code
3-1) Couverture des tests avec rcov
On ne le dit jamais assez, une bonne qualité d’application commence par la qualité de ses tests, mais aussi et surtout par l’exhaustivité de ceux-ci. C’est dans ce contexte qu’intervient rcov : il permet de lancer vos tests et d’en extraire les statistiques de couverture. Un exemple typique d’utilisation est par les tests de contrôleurs (car plus facile de lancer toutes les fonctionnalités de l’application, en en testant tous les points d’entrée. Attention, ce serait une erreur de ne se fier qu’à cela, il est vivement conseillé de ne pas négliger les tests unitaires (tests des modèles).
Voyons comment mettre en oeuvre rcov. Tout d’abord, il faut l’installer. Fort heureusement, il est disponible sous forme de gem standard. La dernière version en date (08/10/2007) est la 0.8.2 :
gem install rcov
La prochaine chose à faire est de désigner les classes de test que rcov va devoir lancer. Dans ce but, nous allons créer un fichier nommé rcov_test_all.rb que nous allons placer dans le répertoire test/. Voici le contenu de ce fichier :
require 'test/unit' require 'test/unit/user_test' require 'test/functional/user_controller_test' require 'test/test_helper'
Dans l’exemple précédent, on indique à rcov de lancer les tests unitaires et les tests de contrôleurs sur le module user. Voici comment lancer rcov :
export RAILS_ENV=test rcov --rails --exclude rcov,rubyforge test/rcov_test_all.rb
L’option –rails permet de ne parser que les répertoires/sources intéressants pour une application Rails. L’option –exclude permet d’exclure du calcul des statistiques des plugins/gems que vous utiliserez dans vos applications (il se peut que vous ayez donc à allonger la liste).
Plus qu’à aller voir le fichier coverage/index.html. Alors, maintenant, quel est votre score ?
3-2) Couverture de la documentation avec dcov
dcov est une application qui permet d’analyser la couverture de vos commentaires rdoc. C’est un outil très utile lorsque vous devez produire du code de qualité dans un projet.
Les rapports produits sont d’une simplicité incroyable : le nom de la classe apparaît en rouge si la couverture totale d’une classe est mauvaise. Si la couverture est différente selon les méthodes d’une classe, alors la granularité s’adapte, et le rapport est plus détaillé (certaines méthodes apparaissent en rouge, d’autres non). Le rapport tient sur une seule page HTML, donc le tout est super simple.
Pour l’installer, faire :
sudo gem install dcov
Pour lancer une analyse de couverture, faire :
dcov -p
Exemple : dcov -p app/**/*.rb va analyser tous les fichiers ruby des répertoires sous app/.
Attention, il existe dans la version courante de dcov (la version 0.2.2) un bug qui empêche la création du rapport. A la ligne 188 du fichier …/gems/dcov-0.2.2/lib/dcov/analyzer.rb, vous trouverez ceci :
output_file = File.open("#{@options[:path]}/coverage.html", "w")
Vous pouvez corriger le problème en changeant la ligne avec par exemple :
output_file = File.open("./coverage.html", "w")
Alors, et maintenant ? Quel est votre pourcentage de couverture de documentation ?