Intégration continue Ruby on Rails

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 %>
	
Connexion à l'application
10 %>
10 %>

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 ?

Répondre

Votre réponse :