🔴 Vos tests unitaires échouent dès que vous bougez le petit doigt...
Pourquoi ? Comment y remédier ?
Cet article fait partie d’un groupe d’articles à propos des tests automatisés. Si vous avez manqué le précédent article, je vous invite à le lire ici : Quels tests automatisés écrire et pourquoi ?
“On n’écrit plus de tests unitaires, car à chaque fois que l’on modifie le moindre petit bout de code, en apparence sans importance, plein de tests échouent ! On perd beaucoup trop de temps à les corriger ou on finit simplement par les supprimer.”
J’entends cette critique à propos des tests unitaires depuis 2013, date à laquelle j’ai parlé des tests unitaires pour la première fois en entreprise au cours d’un stage.
La raison numéro 1 de l’abandon des tests unitaires
La raison principale pour laquelle les équipes ne veulent pas / plus écrire de tests unitaires est donc cette critique injustifiée.
Injustifiée, car elle repose sur une mauvaise pratique, une incompréhension, un quiproquo.
Le sens du mot “unitaire”
Ce quiproquo vient de l’interprétation du mot “unitaire”.
La compréhension que beaucoup en ont est que “unitaire” = “fonction” (ou classe par exemple).
Cette définition galvaudée a de grosses conséquences. Des conséquences néfastes pour un projet puisqu’elle implique l’abandon des tests unitaires.
Rappelons tout d’abord l’objectif d’un bon test unitaire : pouvoir refactorer son code l’esprit tranquille.
Refactorer son code, c’est en modifier les détails d’implémentation, sans en changer le comportement.
Par extension, un test unitaire ne doit pas être couplé à ces détails d’implémentation. “Coupler” signifie que si un détail d’implémentation change, le test doit changer.
Créer un test pour chaque fonction revient à avoir une suite de tests dont le couplage est donc maximum ! Il y a un rapport de 1 pour 1 entre les tests et les fonctions de notre code. Nécessairement, dès qu’une fonction va changer le test devra changer lui aussi…
Pour éviter ces écueils, le mot “unitaire” doit être compris comme suit :
“unitaire” pour unité de comportement. On teste un comportement au sein de l’application, une feature, issue d’une user story par exemple.
“unitaire” pour “isolation”. Un test unitaire doit pouvoir être exécuté en isolation des autres tests. Ils peuvent ainsi être tous exécutés en parallèle.
Comment tester les fonctions privées ?
Les fonctions privées sont, par définition, limitées au contexte de la classe (ou du module) dans lequel elles évoluent. Comme ces fonctions ne sont pas accessibles depuis l’extérieur de la classe ou du module, ce sont donc des détails d’implémentation !
Mais ces fonctions existent, c’est bien parce qu’elles sont appelées à un moment donnée. Elles vont être appelées par des fonctions / méthodes publiques, qui font office d’API publique de notre module / classe.
En testant ces fonctions publiques, on teste donc par transitivité les fonctions privées.
Que faire si ma fonction privée est complexe et que j’aimerais être guidé dans son développement ?
L’avantage des tests unitaires, notamment en Test Driven Development (TDD) est aussi de se sentir guidé dans l’implémentation des fonctionnalités.
Avoir comme seule point d’entrée une fonction publique pour guider l’implémentation d’une fonction privée complexe peut parfois être laborieux.
Heureusement, il existe quelques solutions :
Extraire la fonction privée dans sa propre classe / son propre module
L’une des possibilités est de constater que la complexité de cette fonction privée est telle que c’est probablement le signe qu’elle fait beaucoup de choses, et n’a donc pas une seule responsabilité au sens du principe de responsabilité unique (SRP).
Cela peut être une bonne indication pour sortir la fonction privée de sa classe / son module pour en faire une autre classe / un autre module. Ainsi cette fonction devient “publique” et est donc testable unitairement.
“Pourquoi ne pas avoir rendu directement cette fonction publique en premier lieu au lieu de la garder privée ?”
Parce que ce faisant nous aurions cassé l’encapsulation de la classe / du module. Une fonction privée représente un détail d’implémentation. En décidant de sortir cette fonction dans son propre module, nous avons fait le choix de considérer qu’il existait un nouveau concept à part entière. Cette fonction sera probablement divisée en plus petites fonctions, privées, qui feront office de détails d’implémentation de ce nouveau concept.
Shift Gear Down : changer de vitesse en rendant temporairement la fonction publique
Si l’on considère que l’algorithme de cette fonction privée n’a pas de sens en dehors du module / de la classe, on peut temporairement rendre cette fonction publique pour faire un test unitaire dessus, qui va donc tester le détail d’implémentation.
“Mais je croyais justement qu’il ne fallait pas tester les détails d’implémentation directement ?”
C’est tout à fait juste ! Il faut garder en tête que le plus important en TDD c’est d’avoir un feedback rapide par rapport à l’avancée de l’implémentation. Si ce feedback commence à être trop lent, on peut descendre d’une vitesse “shift gear down”, en créant un test unitaire précisément pour le détail d’implémentation.
Attention toutefois ! Ce genre de test est uniquement destiné à améliorer le feedback instantané. Ce sont des tests fragiles : ils testent des détails d’implémentation, si ces détails changent alors le test va échouer.
Plusieurs options s’offrent alors à nous :
on peut supprimer le test fragile une fois la fonction rendue privée à nouveau, le test initial va de toute façon tester par transitivité cette fonction
on peut laisser le test si on considère qu’il apporte de la valeur pour un futur développeur afin de mieux comprendre l’algorithme (auquel cas il eut été plus judicieux de sortir cette fonction dans son propre module / sa propre classe en premier lieu plutôt que de la laisser publique).
Happy Coding (et Testing) :)
Très bon article. Le genre qui m'aurait fait gagner beaucoup temps quand j'étais étudiant/junior.