La pyramide des tests

SoftMethods_pyramide_de_tests_s

Le concept de pyramide des tests est particulièrement intéressant car il permet de caractériser d’une manière très visuelle sa stratégie de test et de la comparer à ce que devrait être une stratégie idéale. Il a été développé par Mike Cohn, dans son livre « Succeeding with Agile » et a été repris par de nombreux acteurs influents de la sphère Agile.
Commençons tout d’abord par décrire ce qu’est l’anti-pattern typique, encore très présent dans bon nombre de projets: un peu de test unitaire, quelques tests intermédiaires (à un niveau composant ou service) automatisés, ensuite une bonne dose de tests fonctionnels automatisés via l’interface utilisateur et, pour couronner le tout, des jeux de tests manuels eux aussi largement joués via l’interface utilisateur.

Dans ce cas, on est dans le modèle « Cornet de glace » :

SoftMethods_cornet de glace

Je suis frappé de voir combien il y a encore de résistances à la pratique du test unitaire dans bon nombre d’équipes de développement. J’ai tendance à penser que cela procède de deux causes : un manque de connaissance de la pratique et un raisonnement à courte vue qui consiste à estimer que c’est une perte de temps (ce qui est d’autant plus flagrant si les développeurs concernés ne sont pas formés à une bonne mise en œuvre des tests unitaires).
Par ailleurs, l’utilisation des approches efficaces d’automatisation de tests est finalement assez mal connue et il est étonnant de constater que les pratiques consistant à baser l’automatisation des tests sur des scénarii d’interface utilisateur (que ce soit via enregistrement ou codage), largement en vogue il y a une dizaine d’années, sont toujours prévalentes bien qu’ayant prouvé leurs limites (je détaillerai ce point un peu plus loin). La raison en est sans doute qu’au premier abord, leur création semble assez simple et ils ne nécessitent pas de se poser beaucoup de questions.
J’ai vu le cas (sans doute pas si rare que ça) où le cornet n’était pas livré avec la glace… celle-ci a fini par fondre d’ailleurs (les scénarii de tests manuels étaient tellement longs et fastidieux que les tests de non régression n’étaient même plus exécutés…).

En fait, c’est à cela que devrait ressembler la « Pyramide de tests idéale » :

 

SoftMethods_pyramide_de_tests

Mais pourquoi donc ?
Tout d’abord, il est largement admis que plus un bug est détecté tard dans le cycle de fabrication, plus sa réparation coûte cher. Dans le cas extrême du traditionnel cycle en V, un problème dû à une erreur de conception initiale, détecté lors de la phase de test arrivant en fin de cycle, de longs mois après le lancement, peut facilement signifier la mort du projet.
C’est pour cela que les tests unitaires sont à privilégier : s’ils sont bien conçus (nous reviendrons en détail dans un autre article à ce qu’est un bon test unitaire), leur durée d’exécution est courte, ils ne sont pas trop chers à créer et maintenir et ils sont très précis. On peut les lancer très souvent (que ce soit fait par le développeur lui-même ou le système d’intégration continue) : le feedback en cas de problème est donc très rapide (de quelques secondes ou minutes idéalement à quelques heures), avec une identification très précise de l’endroit qui pose problème puisque, par construction, un test unitaire a une portée limitée.
Afin de valider le système au niveau « fonctionnalité », les tests unitaires doivent être complétés par des tests de plus haut niveau, qui mettent en jeu tout un ensemble de composants. Ces tests doivent pouvoir être largement automatisés, mais l’erreur commune est souvent de les effectuer au niveau de l’interface utilisateur du système. Or, si celui-ci est bien conçu, ces tests peuvent et doivent être effectués à un niveau composant ou service (via API, web service, …), et déterminer leur résultat sur la base des données et non de leur présentation. Une bonne pratique à ce niveau consiste à bâtir les tests d’acceptance dès le départ de manière à ce qu’ils soient automatisables: la première fois, ils servent à valider la fonctionnalité, puis sont immédiatement intégrés dans le portefeuille de tests de non régression automatisé du système. Du point de vue des outils, plusieurs options sont envisageables, allant du développement de code spécifique à des outils prêts à l’emploi qui facilitent vraiment le travail et sont directement intégrables dans un processus d’intégration continue avec un feedback efficace des résultats (exemple que j’aime bien: RobotFramework).

Si on effectue ces tests de manière systématique à partir de l’interface utilisateur, on se trompe de cible pour plusieurs raisons. Tout d’abord ces tests vont être nécessairement plus longs à exécuter et plus compliqués à mettre en place (par exemple, les jeux de données de test peuvent être très compliqués à gérer, puisque ils mettent en jeu le système dans sa totalité ; l’installation et le paramétrage peut être aussi un aspect très couteux). En outre, ils mélangent ce qui relève des mécanismes d’interaction avec l’utilisateur avec la logique sous-jacente du système, qui relèvent de couches différentes si l’application est bien conçue. Enfin, la recherche de la cause d’un test qui échoue est potentiellement bien plus longue, puisqu’il faut remonter toute la chaîne des composants mis en jeu ; dans le pire des cas, ces tests peuvent perdre leur caractère déterministe (on ne sait plus trop dire si on peut compter sur leur résultat…). En outre, l’expérience montre que l’interface utilisateur est un domaine souvent assez mouvant: la maintenance des scenarii de tests peut rapidement devenir un cauchemar.

SoftMethods_test_ihm_problème

Ceci ne signifie pas qu’il ne faut pas faire de tests des interfaces utilisateurs, qu’ils soient automatiques ou manuels. Evidemment, c’est souvent un élément essentiel puisque c’est celui avec lequel interagissent les utilisateurs finaux. Les tests automatisés à ce niveau peuvent être ciblés sur plusieurs axes: mécanismes de fonctionnement intrinsèques de l’IHM (de manière aussi découplée que possible du fonctionnel : il peut finalement s’agir de tests unitaires au niveau de l’interface utilisateur…), quelques scénarii de test « end to end » caractéristiques (peut permettre d’éviter la honte du crash dès le lancement du programme pour des raisons sordides de paramétrage ou d’intégration entre composants en dépit de tous les tests effectués par ailleurs). L’expérience montre qu’il est souvent utile d’effectuer en bout de chaine des tests manuels, que ce soit parce que certains critères sont difficilement automatisables (visuels, par exemple), mais aussi parce qu’il ne faut pas oublier de tester « comme le client final », avec un être humain qui interagit avec le système de manière intelligente (par exemple dans une logique exploratoire guidée par un scénario écrit ou une très bonne connaissance du système).
Ceci me permet de conclure sur le fait qu’une stratégie de test efficace, c’est l’assemblage de différents niveaux de filtres qui permettent, par leur combinaison, d’éliminer les problèmes au fil du cycle de développement d’un logiciel : c’est le bon choix et l’assemblage intelligent de ces filtres qui fait une stratégie efficace !