mardi 8 octobre 2013

Tests unitaires : comment en ajouter sur du code existant?

Suite à cette question de Jérôme Avoustin, Nicolas Noullet pose une autre question très intéressante :
"Comment faire avec une énorme base de code sans test ? N'en écrire que pour les bug fixes et nouvelles features ?"
Cette question rejoint celles qui me sont souvent posées lors de mes formations en entreprises ou lors des conférences.

Les tweets étant trop courts pour un tel sujet, je poste quelques éléments de réponses ici, en espérant en éclairer certains, et susciter des réactions ...

Le mieux ...

Avant tout, il faut bien avoir en tête que l'ajout de tests sur du code existant (legacy) est le plus dur !

Un premier point important à prendre en compte est donc le niveau de maturité des développeurs en tests unitaires. S'ils sont débutants, il vaut mieux qu'ils commencent sur du code neuf, pour un nouveau projet.

Mais la réalité du quotidien est qu'on n'attaque pas souvent un nouveau projet ...

Une première approche

Sur un projet existant, la solution peut consister à écrire les nouvelles fonctionnalités non pas dans des classes existantes, mais dans de nouvelles classes qui seront des "collaborateurs" de la classe existante. Le code de cette nouvelle classe sera plus facilement testable, d'autant plus s'il est écrit en TDD . Si le développeur a un niveau suffisant, et que le code existant le permet, l'inter-action entre la classe existante et la nouvelle classe peut être testée en "mockant" la nouvelle classe. La meilleure approche, mais pas du tout la plus simple, étant de commencer par là en TDD bien sûr !
Truc : éviter le statique ! 

Ensuite ...

Une autre approche consiste à écrire des tests unitaires de plus haut niveau, couvrant une plus grande partie du code (mais la moins grande possible ...), "tests d'intégration" ou "tests fonctionnels", en évitant les "tests IHM" (tests par l'IHM). Une fois ces tests écrits, on peut alors se lancer dans un refactoring pour découper et découpler le code existant, pour extraire les traitements dans des classes séparées, en visant le principe "1 classe = 1 rôle", et ainsi aller vers une architecture (plus) testable.

Attention, car pour que ces tests soient "bons" et jouent pleinement leur rôle de "filet de sécurité" pour le refactoring, il est nécessaire d'avoir déjà une bonne expérience des tests unitaires et de ce que sont les "bons" tests ...
Truc : ciblez le code de traitements algorithmiques, plus facile à tester unitairement, et attaquez vous plus tard au code qui interagit avec une base de données ou un équipement extérieur ...

La couverture de code

Pour ceux qui auraient un doute, il s'agit de mesurer le % de code exécuté en jouant un test, une série de tests, ou tous les tests. Mais les outils pour cette mesure peuvent nous fournir une autre information, complémentaire et extrêmement intéressante : quel code est exécuté (en général en vert), et surtout, quel code n'est pas exécuté (en rouge), donc pas couvert par les tests.

Dans la première approche évoquée précédemment, le code nouveau, ou extrait d'une classe, doit impérativement être bien couvert.

Dans la seconde approche, la visualisation des lignes de code, des classes, voire des packages non couverts, peut nous aider à renforcer les tests de haut niveau pour garantir un meilleur filet de sécurité pour le refactoring à venir.

Conclusion

Les tests unitaires sont indispensables ! Se mettre à en écrire n'est pas facile. Le TDD est la meilleure solution, c'est une partie intégrante de la pratique de développement (et de conception), mais c'est véritablement un "état d'esprit" qu'il faut acquérir par la pratique et l'entrainement.

Accrochez-vous, ça vaut le coup !

5 commentaires:

  1. D'où l'intérêt de la métrique "couverture sur le nouveau code" d'un outil comme SonarQube, qui évite de se décourager au vu de l'immensité de la tâche de couverture du code hérité.

    RépondreSupprimer
    Réponses
    1. Merci J.B d'évoquer la couverture de code, je complète mon article avec une idée que j'avais mis de côté

      Supprimer
  2. Sujet intéressant, c'est une vraie compétence que de savoir faire évoluer du legacy en toute sécurité. Il est nécéssaire de bien connaitre certain pattern de tests et les techniques de refactoring. Je recommande le bouquin "Working Effectively with Legacy Code" de Michael Feathers. Il décrit des techniques efficaces que j'ai expérimenté. Travailler dans le Legacy peu devenir amusant avec les bonnes pratiques !!
    ++

    RépondreSupprimer
    Réponses
    1. Merci Luc pour ton commentaire ! Tout à fait d'accord avec toi : les patterns et les techniques sont d'une grande aide, on peut les appréhender au travers de nombreux livres, et seul la pratique et l'entrainement permettront de les maîtriser pour savoir les utiliser à bon escient. Et si on peut s'amuser avec du Legacy : cool ! ;-)

      Supprimer
  3. D'autre réactions sur la communauté "French Software Craftsmanship" de G+ : https://plus.google.com/117142607049442069106/posts/EYrKXt3nvQk

    RépondreSupprimer