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. 

 

 

framework pour les tests End to End

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...).

 

 

 

Les principes First

 

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 :

  • F - Fast (Rapide) : Un TU doit être rapide à exécuter (quelques millisecondes),
  • I - Isolated (Isolé) : TUs doivent être indépendants les uns des autres,
  • R - Repeatable : Un TU doit pouvoir être exécuté plusieurs fois avec les mêmes résultats,
  • S - Self-validating (Auto-validant) : Le TU doit avoir un résultat clair et automatique. Les assertions qui valident le comportement du code doivent être explicites,
  • T - Timely (Opportun) : Les TUs doivent être écrits au moment où le code est déevloppé ou peu de temps après. Attendre trop longtemps pour écrire des tests peut mener à des erreurs non détectées et à des retards dans le processus de développement.

 

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

 

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.

 

  1. Arrange (Préparer) : Initialisation des variables/objets nécessaires à la mise en place des préconditions pour le test,
  2. Act (Agir) : Appel de la méthode ou fonction à tester,
  3. Assert (Vérifier) : Vérification du résultat obtenu.

 

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 :

 

  • Écrire le test : Avant de coder, le développeur écrit un test unitaire pour une nouvelle fonctionnalité. Ce test définit le comportement attendu du code,
  • Exécuter le test : Ce dernier doit échouer (logiquement),
  • Écrire le code : Le développeur écrit ensuite le code nécessaire pour faire passer le test,
  • Exécuter le test : On exécute le test à nouveau, qui doit réussir.
  • Refactoriser : Une fois le test passé, on peut améliorer le code (refactoring) tout en s'assurant que tous les tests continuent de passer.

 

 

 

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 :

 

  1. Couverture de lignes : Mesure le pourcentage de lignes de code exécutées par les tests.
  2. Couverture de branches : Évalue le pourcentage de branches (conditions if/else) qui ont été exécutées par les tests.
  3. Couverture de fonctions : Mesure le pourcentage de fonctions/méthodes qui ont été appelées pendant les tests.

 

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.

 

 

 

Comment différencier les tests unitaires des autres types de tests ?

 

  • Test d’intégration : Ils ont la même forme que les tests unitaires, mais se font sans Mock (il reste possible de simuler le comportement d’un Back Office avec un Json Serveur, par exemple) où le test va parcourir l’ensemble des couches du produit,
  • Test End-To-End : Ces tests nécessitent un environnement dédié avec des scripts qui vont permettre d’initialiser les datas et d’assurer la répétabilité. Les Frameworks les plus courants sont Selenium et Cypress.
  • Smoke Test : C’est un type de test qui consiste à vérifier rapidement si les fonctionnalités de base d'une application fonctionnent correctement après une modification, comme une mise à jour ou un déploiement. Ils servent d'alerte précoce pour s'assurer que l'application est stable avant de passer à des tests plus approfondis.
  • Test UI : Les tests UI (User Interface) sont des tests qui se concentrent sur l'interface utilisateur d'une application. Ces derniers sont souvent des tests manuels et permettent une validation des métiers.

 

 

 

Conclusion :

 

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.