Magazine

Tests unitaires et intégration continue

Publié le 18 mars 2009 par Abouchard

Je vais vous parler de deux notions qui sont assez connexes, et que certaines personnes ont tendance à mélanger un peu.

Les tests unitaires

Le but des tests unitaires est d'automatiser les tests de non-régression. Quand on développe un logiciel, on peut facilement faire des modifications sans se rendre compte qu'elles introduisent des bugs dans certains cas particuliers. C'est ce qu'on appelle des bugs de régression, en ce sens qu'on introduit de nouveaux bugs à la suite d'une évolution fonctionnelle. Comme il n'est pas humainement possible de tester systématiquement tous les cas d'utilisation possible, ces bugs peuvent se retrouver déployés en production, avec tous les problèmes que cela comporte.

Une des notions importantes, quand on fait du développement, est de savoir que plus un bug est détecté tôt, plus il sera facile à corriger. Les gros soucis arrivent quand on fait du "sur-développement" par-dessus un bug de régression ; il devient très difficile de faire un retour en arrière sur le code incriminé, car cela impliquerait de supprimer des fonctionnalités. Il faut alors corriger ce nouveau code, en essayant de trouver une aiguille dans une botte de foin.

Concrètement, les tests unitaires consistent en un ensemble de scripts, qui ont chacun en charge la validation d'un morceau de code (ou d'une classe si vous faite de la programmation orientée objet). Il est assez évident de tester de la sorte les bibliothèques de fonction : les "librairies" peuvent être vues comme des boîtes noires, avec des entrées et des sorties. Il suffit d'injecter certaines données en entrée, et vérifier la conformité de ce qu'on obtient en sortie.
Pour du code applicatif, c'est un peu plus délicat, car on est parfois obligé de mettre en place un environnement de test sophistiqué (bases de données avec lots de test, génération de données aléatoires, gestion des tentatives de hack, ...). Mais bien heureusement il existe de très nombreux frameworks pour vous aider à y arriver ; suivant votre plate-forme de développement, vous pouvez vous intéresser à JUnit (Java), SimpleTest (PHP), CppUnit (C++), JSUnit (Javascript), ASUnit (ActionScript), Test::Unit (Ruby), Test::Unit (Perl), unittest (Python), NUnit (.NET), ...

Au cours du développement, il faut évidemment prendre le temps de réaliser les tests équivalents à chaque nouvelle fonctionnalité, à chaque nouveau cas particulier. Mais ce temps se récupère au moment du test et du débuggage. Il suffit de lancer les tests unitaires pour savoir rapidement où les bugs se situent, ce qui permet de les corriger sans perdre de temps.

L'intégration continue

Comme je le disais en introduction, certaines personnes confondent les tests unitaires avec le processus d'intégration continue. En fait, les tests unitaires sont habituellement intégrés à l'intégration continue, ils en constituent l'une des étapes.

Le principe de l'intégration continue est d'aller encore plus loin dans l'automatisation des vérifications ; si les tests unitaires se concentrent sur la non-régression du code, l'intégration continue prend en compte tous les aspects d'un développement logiciel. Le but est toujours de détecter les problèmes le plus tôt possible, pour s'assurer qu'ils soient résolus le plus vite possible.

Une plate-forme d'intégration continue s'exécute régulièrement, avec une fréquence la plus courte possible (au minimum toutes les nuits, au mieux plusieurs fois par jour). À chaque exécution, elle réalise plusieurs actions :

  • Récupération du code source, depuis le dépôt qui est la plupart du temps un outil de gestion de source (CVS, SVN, Git, SourceSafe, ...). On peut éventuellement vérifier le nombre de fichiers ou la taille globale d'un projet, pour s'assurer qu'il n'y a pas eu de suppressions intempestives.
  • Vérification et optimisation du code. Cette étape n'est pas obligatoire, mais l'exécution automatique d'un outil d'optimisation du code (comme lint, Jlint, php-sat, ...) est une bonne idée, car elle assure un minimum de qualité sur le code écrit par les développeurs.
  • Compilation des sources (pour du code en Java ou en C++, par exemple). Si un projet ne peut pas être compilé complètement, il faut remonter rapidement l'information, pour que le code problématique soit corrigé. Un projet qui ne compile pas peut bloquer l'ensemble des développeurs qui travaillent dessus.
  • Exécution des tests unitaires. Cette étape a été décrite précédemment, je ne vais pas revenir dessus.
  • Packaging de l'application. Cela peut prendre de nombreux aspects, suivant le type de logiciel que l'on développe. Pour un logiciel "lourd", on va tenter d'en générer automatiquement l'archive d'installation ; pour un service Web, on va préparer les fichiers pour leur mise en ligne.
  • Déploiement de l'application. Là encore, tout dépend du type de logiciel. Cela peut aller de l'installation automatique du logiciel sur une machine, jusqu'à la mise en ligne d'un site Web. L'idée est qu'un logiciel qu'on ne peut pas installer ne sert à rien. S'il y a un souci avec les procédures d'installation, ou une incompatibilité entre ces procédures et la nouvelle version du logiciel, il faut corriger cela.
  • Exécution des tests fonctionnels. En reprenant les mêmes outils que les tests unitaires, il est possible de vérifier un grand nombre de cas d'utilisation.

En plus de tout cela, on ajoute souvent une étape supplémentaire, même si elle ne fait pas partie de l'intégration continue à proprement parler. Il s'agit de la génération automatique de la documentation de développement, qui est faite à partir des informations présentes dans le code source (grâce à des outils comme JavaDoc, PHPDoc, Doxygen, HeaderBrowser, ...). Il est toujours plus facile de développer quand on a sous la main une version à jour de la documentation.

À la fin de l'exécution de toutes ces actions, la plate-forme doit envoyer des messages aux personnes concernées par les problèmes relevés. Ces messages ne doivent pas se transformer en spam, car ils deviendraient inutiles (personne n'y ferait plus attention). Il faut donc faire attention à remonter les vrais problèmes, et ne pas mettre tous les développeurs en copie sauf dans les cas nécessaires.
La résolution des bugs remontés doit être la priorité première d'une équipe de développement. C'est simple, si on continue à développer en sachant qu'il y a des bugs, on sait pertinemment qu'il faudra encore plus de temps et d'énergie pour les corriger, tout en risquant de devoir refaire les "sur-développements".

En bout de course, l'application déployée par l'intégration continue doit être accessible à l'équipe de test, qui peut ainsi procéder à ses vérifications complémentaires sans avoir à se soucier des étapes techniques en amont (compilation, packaging, déploiement).

Mon expérience

Mettre en place une plate-forme d'intégration continue est un tâche technique assez longue. Mais une fois que c'est fait, c'est à la fois un confort de travail et une sécurité dont on ne peut plus se passer.

L'écriture des tests unitaire est quelque chose d'un peu fastidieux, qu'il est souvent difficile d'imposer à une équipe qui a pris de mauvaises habitudes. Un développeur ne voit souvent le code source comme seul élément constitutif de son travail, et oublie la documentation et les tests. Encourager l'écriture de tests unitaire est un travail de longue haleine sur lequel il faut maintenir une pression constante, sous peine de laisser prendre la poussière. Et un test qui n'est pas tenu à jour devient rapidement inutile.

Une dernière chose : L'intégration continue est souvent associé à la pratique de méthodes agiles. Pourtant, c'est un principe assez ancien, qui peut être utilisé même dans le cadre d'organisations non agiles.


Retour à La Une de Logo Paperblog

A propos de l’auteur


Abouchard 392 partages Voir son profil
Voir son blog

l'auteur n'a pas encore renseigné son compte l'auteur n'a pas encore renseigné son compte