Pour faire suite à l’article “La stratégie de test, c'est quoi ?”, nous allons nous intéresser dans cet article, aux bonnes pratiques quant à la mise en place des Tests Unitaires dans une codebase.
Les tests unitaires constituent une étape fondamentale dans le processus de validation d'un produit, et est souvent représenté comme la première étape de la pyramide de la stratégie globale des tests.
Source: https://www.testbytes.net/blog/end-to-end-testing/
Leur rôle est de vérifier que chaque composant ou "unité" du code fonctionne correctement de manière isolée, sans dépendre des autres parties du système. Les tests unitaires se concentrent sur la logique interne des méthodes ou des classes, offrant ainsi une première ligne de défense contre les régressions.
Cela étant dit, nous allons maintenant nous attaquer aux bonnes pratiques à respecter afin de pouvoir rédiger des Tests Unitaires pour un code de qualité, et de ne pas les confondre avec les autres types de tests (intégration, end-to-end, fonctionnelle, etc...).
Le principe FIRST a été popularisé par Robert C. Martin, également connu sous le nom de Uncle Bob. Il est un pionnier du développement agile (co-auteur du Manifeste Agile) et des bonnes pratiques de programmation, notamment dans le domaine des tests.
Chaque lettre de l'acronyme FIRST représente un aspect clé de ce que doit être un Test Unitaire :
Ainsi, selon le principe FIRST, un test unitaire doit être rapide, indépendant, reproductible, et facile à comprendre. Chaque test doit être conçu de manière à ne dépendre d'aucun autre, avec des résultats identiques à chaque exécution, sans intervention manuelle.
L’isolation des TUs se fait à travers des Mocks, c’est à dire un objet simulé qui imite le comportement d'une dépendance réelle d'un système. Il permet de remplacer des composants externes (comme des bases de données ou des API) afin de contrôler et prédéfinir leurs réponses, facilitant ainsi l'observation du comportement du code sans les interférences des dépendances réelles.
L'assertion est une instruction dans un test qui vérifie si une condition est vraie ou fausse. Elle permet de comparer le résultat attendu à celui obtenu lors de l'exécution du code testé, et si la condition n'est pas remplie, le test échoue, indiquant une anomalie dans le comportement du code.
Le modèle AAA (Arrange, Act, Assert) s'est développé progressivement au sein de la communauté des développeurs et testeurs, comme une convention naturelle pour organiser et structurer les tests unitaires de manière claire et lisible.
Ce modèle apporte donc de la lisibilité à vos tests unitaires et les rend plus facilement maintenable. De plus, en adoptant le AAA, vous vous assurez de ne jamais oublier d’assertion (SonarQube vous remerciera).
Méthodologie TDD
Le TDD (Test-Driven Development), ou développement dirigé par les tests, est une méthodologie qui se concentre sur l'écriture de tests avant d'implémenter le code fonctionnel. Voici ces étapes clés :
Source : https://gayerie.dev/docs/testing/test/tdd.html
Il est important de noter que dans une approche TDD, il faut souvent plusieurs cycles pour refactoriser correctement le code, ce qui permet de prendre du recul sur le code que nous produisons, et de se rapprocher aux mieux des Design Patern, ou tout autre bonne pratique de dev définit par l’équipe, rendant souvent le code plus lisible tout en facilitant les Merge Request.
Calculer et suivre la couverture de code
La couverture de code est une métrique qui mesure la proportion de code source qui est testée par des tests automatisés. Elle aide à identifier les parties du code qui ne sont pas couvertes par des tests.
La couverture est souvent découpée en 3 types :
Des outils intégrés à chaque IDE permettent de suivre la couverture des tests unitaires durant le développement. Dans un contexte d'intégration continue, SonarQube est souvent utilisé pour mesurer la couverture globale.
Bien qu'il n'existe pas de couverture idéale — le 100 % étant généralement inatteignable en raison des couches que les tests unitaires ne peuvent ou ne doivent pas tester — la couverture globale peut correspondre à l'agrégation de chaque catégorie de tests (unitaires, d'intégration et end-to-end).
Il est possible de viser un taux de couverture de 80 à 90 % en excluant des couches isolées, à l'aide d’expressions régulières simples (ex : *Dao*, *Dto*, *Component*, *Page*, etc...).
L'objectif de la couverture des tests unitaires doit être partagé au sein de l'équipe de développement. Cela permet d'identifier les méthodes à couvrir et, dans une démarche TDD, de déterminer quand et sur quelles parties du code il est préférable de concentrer les efforts.
Pour bien écrire des tests unitaires, il est essentiel de suivre le principe FIRST (rapide, indépendant, reproductible et facile à comprendre/prendre en main) et l’approche AAA (Arranger, Agir et Assertion).
Il est crucial de garder à l'esprit que les tests unitaires ne sont ni des tests d'intégration, ni des tests d'interface utilisateur. Leur objectif est de prévenir les régressions du code dit métier (le TDD facilitant les cycles de refactorisation), et de rester répétables et isolés, notamment via l'utilisation de Mocks des différentes couches techniques du produit.
Les tests unitaires peuvent également servir de documentation dans un contexte de Clean Code / Agile. En effet, en pratiquant le TU en AAA, et surtout en détaillant les tenant et aboutissant du test dans la signature de la méthode (ou en commentaire), les développeurs peuvent rapidement comprendre à quoi servent les méthodes en lisant les tests unitaires.