☠️ Sus aux mocks : Vive les tests de bout-en-bout 🙌

Rémi Blaise
7 min readJan 10, 2021

--

🏁 Pas le temps de tout lire ? Passe directement au résumé ! 🏁

Peut-être as-tu suivi mes péripéties d’une malencontreuse mésaventure avec un test unitaire.

Si tel est le cas, tu as en tête l’archétype de l’anti-modèle qu’on cherche à éviter : le test unitaire pleinement isolé, dont on a simulé toutes les dépendances et qui verrouille notre implémentation.

Il peut être facile de s’égarer sur le chemin des tests unitaires. Je te propose de revenir aux bases.

❓ À quoi sert une suite de tests ?

À la base de la base, quel est notre besoin ? Pourquoi veut-on faire une suite de test ?

La raison primaire est d’automatiser les traditionnels tests de bout-en-bout manuels réalisés par le Product Owner (anciennement appelé Maître d’Ouvrage) à l’étape de la recette. En effet, un inconvénient majeur se pose à l’exécution manuelle d’un cahier de test : c’est un processus coûteux en temps de travail. Il est donc impensable de le ré-exécuter en intégralité à chaque nouvelle fonctionnalité ou évolution, et encore moins à chaque commit (modification du code).

Note en passant : Le traditionnel recettage possède néanmoins un atout : la flexibilité et l’intelligence d’un humain qui peut répérer des choses imprévues, s’adapter. L’autre face de la pièce est que l’humain est faillible.

Le résultat d’une exécution manuelle toujours partielle du cahier de test est qu’il est toujours possible qu’une régression ou un dysfonctionnement passe à travers les mailles du filet. En effet, dans une base de code monolithique, toute évolution peut avoir un impact potentiel sur toute fonctionnalité déjà présente.

✔️ Quelles sont les qualités recherchées ?

Le besoin de la suite de test idéale est donc le suivant :

  1. Tester chaque fonctionnalité de bout-en-bout : on rend impossible tout dysfonctionnement ainsi que toute régression.
  2. S’assurer que la solution concrète réponde aux exigences fonctionnelles (= métier).

❌ Quels sont les défauts redoutés ?

Une mauvaise suite de test présente les défauts suivants :

  1. Elle est chronophage en temps de développement, donc pas rentable pour le client.
  2. Elle est dépendante à l’implémentation, et va donc devenir incompatible à chaque évolution. Pour les connaisseurs, c’est ce qu’on appelle en Machine Learning du sur-apprentissage : notre suite est sur-spécialisée, plutôt que de s’assurer que notre code répond aux exigences métier, elle vérifie que le code reste identique à celui qu’on a écrit.

Tu l’as reconnu, ce sont les 2 défauts identifiés chez notre anti-modèle.

Il y a donc des signes, des drapeaux rouges faciles à identifier, qui ne trompent pas :

  1. 🚩 Ton test représente plus de lignes de code que ton sujet.
  2. 🚩 Ton test t’annonce des faux-positifs : il casse alors que tu respectes toujours l’exigence métier.
  3. 🚩 Tu observes dans l’arborescence Git ce genre de message de commit :

Alors que tu devrais plutôt avoir : “Fix code that was breaking use case”.

Avis aux managers : ce genre d’intitulé est le témoin d’une perte de temps chez vos développeurs.

🏆 Comment mesurer la qualité d’une suite de test ?

Deux indicateurs clés permettent de s’assurer de la qualité d’une suite de test :

  1. Le taux de couverture fonctionnelle permet de mesurer la résilience au changement.
  2. Le taux de couverture technique permet de mesurer la résilience aux cas particuliers, aux exceptions imprévues.

Le taux de couverture fonctionnelle s’obtient en comparant le nombre de règles testées au nombre présent dans le cahier des exigences, (aka. spécification fonctionnelle, cahier des charges, cahier de test, …). Notons que l’utilisation d’une pile de tâches n’est pas suffisante et nécessite de s’accompagner d’un document de suivi des exigences fonctionnelles. La spécification des exigences en Gherkin peut aider à la bonne communication entre le responsable métier et le responsable technique et se substituer à la tenue d’un tel document. De plus, la mesure de la couverture fonctionnelle permet d’impliquer le responsable métier l’élaboration de la suite de tests.

Le taux de couverture technique s’obtient en comparant le nombre de lignes de code parcourus lors de l’exécution de la suite de tests au nombre présent dans le projet.

Test en Isolation 🆚 Test en Intégration

Une partie des erreurs, possiblement à l’origine d’un échec, sont les erreurs d’intégration avec le socle logiciel (frameworks et bibliothèques) ou les systèmes extérieurs (APIs partenaires). En simulant ces systèmes dont tu n’es pas l’auteur, dont tu n’as pas la responsabilité, tu remplaces le comportement réel du système par son comportement imaginé, tel que tu l’as compris. Tu empêches donc ton test de te prévenir de tes erreurs de programmation.

La règle est simple : on doit s’assurer au maximum que la solution fonctionne en situation réelle, ie. en intégration avec les systèmes extérieurs.

Un credo simple à garder en mémoire : dans la mesure du possible,

Ne mock pas ce qui le t’appartient pas.

En particulier, tu ne dois jamais simuler les bibliothèques et frameworks que tu utilises, tu te priverais d’être averti de leur mauvaise utilisation.

Tu ne dois simuler les systèmes extérieurs que lors que ceux-ci n’offrent pas d’environnement de test approprié.

“Quid dans ce cas des dépendances qui m’appartiennent ?”

La section suivante va répondre à ta question.

Test de Bout-en-bout, Unitaires ou Intermédiaires : que dois-je choisir ? 🤔

On l’a dit, nous voulons que chaque fonctionnalité soit testée de bout-en-bout.

“Mais alors, dois-je bannir les tests unitaires et ne faire que des tests de bout-en-bout ?”

C’est là que ce trouve une subtilité. En effet, on peut très bien tester une fonctionnalité d’un bout à l’autre par une suite de tests unitaires ou intermédiaires, couvrant chacun une portion du périmètre technique.

Je m’explique via un schéma. Admettons que tu as une fonctionnalité qui se traduit par l’application successive de 3 composants techniques. Tu as alors 3 solutions pour tester ta fonctionnalité de bout-en-bout :

Cependant, il faut être conscient que plus ton test est découpé en une multitude d’étapes, moins il est robuste : tu t’éloignes du métier, tu augmentes la complexité, il devient volumineux et moins maintenable, il dépend de tes détails d’architecture (tu simules les composants A et C pour tester le composant B), l’information qui forme tes tests commence à répliquer celle qui forme ton code et tu multiplies les chances qu’une erreur de conception se trouve répliquée par le test.

De plus, ce schéma met en évidence une notion importante : tu n’as pas besoin d’avoir des tests de bout-en-bout ET des tests unitaires : il suffit de l’un OU l’autre pour couvrir ta fonctionnalité ! Et oui, un test de bout-en-bout test bel et bien que ton code fonctionne.

Alors, bout-en-bout ou unitaire ? Tu l’as compris, dans 98% des cas il faut privilégier des tests de bout-en-bout.

Mais où diable sont donc passés les 2% restants ?

Cas particulier : quand utiliser un test unitaire 🦓

Il existe uniquement 2 situations justifiant un test unitaire :

  1. Un composant implémente une algorithmie complexe : le fonctionnement correct du composant n’est pas évident à la simple lecture du code.
    Cela peut être parfois le cas d’un ensemble de règles métier.
    Par exemple : un composant implémente un ensemble de règles de calcul d’un taux de TVA se conformant à une règlementation complexe avec des paliers, taux correcteurs, etc.
  2. Un composant est partagé par de nombreuses fonctionnalités ou est destiné à être réutilisable. Il est à destination des autres développeurs et est un vecteur particulier de régression potentielle, due à l’évolution d’une fonctionnalité qui impacte le composant, qui impacte à son tour les autres fonctionnalités.

Ces 2 caractéristiques sont les 2 seules raisons valables d’écrire des tests unitaires. Voilà qui devrait en réduire considérablement le nombre dans nos projets. En suivant cette philosophie, l’écriture d’un test unitaire pour un composant simple, d’importance mineure, comme découverte dans mon précédent article, deviens une hérésie impensable.

Parfois, un composant peut remplir les 2 critères à la fois.
Par exemple, le parseur du composant Yaml de Symfony est algorithmiquement complexe : il ne suffit pas de le lire pour être convaincu de son bon fonctionnement. C’est également un composant utilitaire, utilisé par de nombreux autres composants et applications. Ainsi, des tests unitaires sont essentiels.

⌨️ Un exemple concret en TDD : ton test d’abord

Le principe du TDD est simple : écrire ton test avant ton code va t’aider à

  • écrire ton code en te donnant un objectif clair,
  • éviter de sur-spécialiser ton test,
  • t’empêcher d’avoir la flemme ou de ne pas prendre le temps.

Je suis chargé d’une fonctionnalité. Avant de plancher sur le code, je crée un test de bout-en-bout :

Tu obtiens une belle pastille rouge : ton code est prêt à être écrit ! Objectif : obtenir une pastille verte, suite à quoi tu peux soumettre sans réfléchir, en toute confiance !

Si tu as correctement transcrit chaque règle métier en une règle de test, tu devrais obtenir 100% de couverture fonctionnelle et près de 100% sur la couverture technique !

Prouuuu ! Le valeureux chevalier transperce de son épée le monstre gluant, qui disparaît dans les tréfonds du néant.

En résumé

  • La raison d’existance des tests automatisés est de m’épargner le travail d’ouvrir mon application pour m’assurer qu’elle fonctionne toujours. On automatise le travail de recette.
  • Une suite idéale teste chaque fonctionnalité de bout-en-bout : elle prévient les dysfonctionnements et les régressions et s’assure que la solution réponde aux exigences métier.
  • Une mauvaise suite est chronophage et verrouille l’implémentation. Il existe des symptômes 🚩 faciles à repérer.
  • La couverture fonctionnelle mesure la résilience au changement, tandis que la couverture technique mesure la résilience aux cas particuliers.
  • Les tests doivent être réalisés en intégration, avec le moins de simulacres possibles. “Don’t mock what you don’t own”
  • Dans 98% des cas, réalise un seul test de bout-en-bout.
  • Réalise un test unitaire si ton composant logiciel présente une algorithmie complexe ou est réutilisé plusieurs fois.

--

--