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.
Bonjour,
Entièrement d’accord avec l’ensemble de l’article.
Dans l’optique de simplifier la mise en place d’une plate-forme d’intégration continue, décrite ici comme « tache technique assez longue » une équipe a mis à disposition un serveur virtuel d’intégration continue sous licence GPL disponible ici : pnptechnologies.free.fr.
Il est facile à installer et à tester et justement vos retours nous intéressent.
Toutes nos excuses pour la communication sauvage, je n’ai pas trouvé le mail M. Bouchard. Modérez-moi si nécessaire ou hors-propos.
Effectivement, vous n’êtes pas très discret dans la promotion de votre produit. 😉
Je vais quand même laisser votre commentaire, il pourrait intéresser certaines personnes.
Très intéressant, cela étant est-il possible dans une démarche agile de lié un gestionnaire de source SVN en l’occurrence à un bugTracker de telle sorte à automatiser la création de issue/bug automatiquement après création des test unitaires ?
Les plate-formes d’intégration continue intègrent toutes un système de gestion des remontées, avec traçage des bugs et de leurs résolutions. Certaines s’intègrent à des buglists générales, mais pas tous.
À vous de voir quel est l’outil qui vous convient le mieux, mais vous pouvez regarder Bamboo (par Atlassian), qui s’intègre avec l’outil de buglist Jira du même éditeur.
L’article m’a expliqué beaucoup de choses qui n’étaient pas clairs.
Est ce que vous pouvez nous donner quelques exemples de plateforme d’intégration continue qui serent à gérer les projets software suivant la méthodologie agile.
Ce sont deux notions différentes. L’intégration continue est un outil, l’agilité est une méthode.
La plupart des méthodes agiles conseillent de mettre en place de la revue de code automatique, et pour cela on utilise l’intégration continue.
Juste une petite remarque sur : Le but des tests unitaires est d’automatiser les tests de non-régression.
Absolument pas.
Les tests unitaires consistent à valider un nouveau morceau de code (une nouvelles functions, procédures). Il s’agit bien de validation unitaire : s’assurer que le code fait bien ce que l’on attend de lui.
La non-régression, ce sont des tests différents dit justement « tests de non-régression ».
Là, il s’agit de s’assurer qu’après un nouveau build, on n’obtient pas un comportement différent de celui observé auparavant (avant ajout ou modification sur l’existant).
Cordialement.
Je comprends la différence entre les deux, mais elle reste très subtile. C’est pratique de pouvoir tester automatiquement qu’un nouveau code fait ce qu’on attend de lui. Associé aux TDD, ça accélère les développements.
Mais cela révèle vraiment toute son utilité quand on envisage le code comme étant quelque chose de vivant dans la durée. S’il faut faire des ajouts fonctionnels qui impactent ce même morceau de code, on pourra vérifier qu’il fonctionne toujours grâce aux tests unitaires réalisés préalablement.
Concrètement, je ne connais personne qui écrive des tests unitaires différents de leurs tests de non-régression. Peut-être que la non-régression sera gérée en ajoutant des tests supplémentaires, mais la séparation entre les deux reste floue.
La différence est bien plus claire entre les tests unitaires (est-ce que chaque brique fonctionne comme elle le doit ?), les tests d’intégration (quand on met toutes les briques ensemble, est-ce que ça répond à la spec technique ?) et les test fonctionnels (est-ce que la spec fonctionnelle est validée ?).
Lorsque j’explique aux réalisateurs l’étape de tests unitaires, je leur indique que c’est un ensemble de cas de tests couvrant un certain pourcentage de code (je rentre pas dans le % c’est un autre débat).
Dans cet ensemble de tests, il va exister des tests qui couvrent les adaptations du composant et des tests qui couvrent le code qui n’a pas évolué. Les tests unitaires est l’ensemble des tests d’évolution et de non régression.
Pour les tests de non régression, normalement on doit réutiliser les cas de tests déjà préparés dans les évolutions précédentes du composant. Pour les tests d’adaptation, on peut très bien créer ou réutiliser des cas de tests. Ceci dépend de la nature de l’évolution du composant.
Ensuite ce n’est qu’une manière de présenter les test ^^