Faire des tests unitaires est impossible.

1

A la question “qu’est ce qu’un test unitaire” la réponse  la plus fréquemment donnée est “c’est un test qui teste une fonction en isolation du reste du code”. Or cette définition est fausse ! Afin de le démontrer, prenons un cas simple : nous voulons tester la fonction add de la classe BigDecimal.

Nous écrivons alors le test suivant :

Au premier regard, tout va bien, nous ne testons que la fonction add. Mais en y regardant de plus près, nous testons aussi le constructeur! Certes «le constructeur est trivial et ne fait rien de compliqué», n’est-ce pas ? Vu de l’extérieur, oui, mais si nous regardons le code, le constructeur fait beaucoup de choses !

A ce stade de la démonstration, il est fréquent d’entendre «oui mais les constructeurs ne sont pas vraiment des fonctions» ou encore «le constructeur est une fonction particulière, elle fait exception à la règle des tests unitaires». Pour l’exercice, admettons.

Regardons de plus près cette assertion : elle utilise la fonction equals ! Cette fois ci, pas d’exception qui tienne ! Equals est une fonction comme une autre (beaucoup plus difficile à écrire correctement qu’il n’y paraît d’ailleurs) !

Au final, si le test est rouge, cela peut venir de trois endroits différents :

  • le constructeur
  • la méthode add
  • la méthode equals

Nous sommes dans un cas très simple, loin des complexités réelles de vrais projets, et pourtant, nous avons déjà cassé notre définition.

Tout ceci n’aurait rien de grave si nous nous arrêtions là. Cependant, cette définition nuit gravement au code. Beaucoup de tests sont écrits aujourd’hui dans l’espoir de réussir à respecter cette définition. Du coup, on s’autorise des entorses «acceptables» :

  • Les constructeurs ne sont pas de vraies fonctions
  • equals / hashcode non plus
  • toute fonction triviale (aka getters et setters) est autorisée

Les deux premiers points sont faux mais pas dramatiques. Le vrai problème réside dans le troisième point car, sous prétexte de vouloir faire des tests unitaires (ce qui est très bien !), nous dégradons (fortement) le design de notre code (ce qui est très mal !).

Imaginons cela sur notre exemple : nous pouvons alors vérifier que la fonction add est correcte en vérifiant l’état de l’objet grâce aux getters.

Sauf qu’à faire des getters et des setters partout :

  • on casse l’encapsulation de nos objets (oui l’encapsulation ne consiste pas juste à avoir tous ses champs privés mais bien à cacher les détails d’implémentation)
  • on lie ses tests à l’implémentation de la solution

Ces deux points ont deux conséquences :

  • vous ne faites plus de l’objet mais du procédural
  • vous ne pouvez plus refactorer à votre aise

Si vous regardez d’un peu plus près l’implémentation de BigDecimal, vous comprendrez très vite pourquoi vous ne voulez pas que votre code de test (et encore plus votre code de production) soit conscient des détails d’implémentation de l’objet !

Beaucoup de définitions différentes de «test unitaire» existent et le but de cet article n’est pas de donner une «bonne» définition, mais bien d’alerter sur les dangers de la définition la plus commune.

En espérant vous avoir convaincu d’utiliser une autre définition !

P.S: la définition actuellement utilisée par l’auteur est : «Un test est unitaire si le code sous test ne dépend que du langage (pas de framework, pas de base de donnée,…)»

Partagez cet article.

A propos de l'auteur

Xavier travaille comme développeur et leader technique Java depuis plusieurs années et suis de très près les nombreuses avancées dans le domaine. Expert sur Struts, Spring, Hibernate, JUnit, TestNG, Mockito, Apache commons, Xavier est aussi adepte du Craftsmanship. "La pédagogie et le partage des bonnes pratiques dans un esprit convivial et ludique" est bel et bien son crédo.

Un commentaire

  1. Personne n’est vraiment d’accord sur la définition des tests unitaires. C’est de toute façon récurent dans notre profession, personne n’est d’accord sur les best practices. À partir du moment où les tests sont suffisamment petits pour être capable de déterminer rapidement la source d’un échec, alors le test est unitaire. À chacun de définir « rapidement » dans le contexte de son projet… Ensuite il faut les rendre rapides, répétables, automatisés, faciles a initiés (trop de lignes de setup est un code smell. Mais trop c’est combien ?) … La rapidité d’un test est elle aussi relative. Peu importe que chaque test prenne 50ms si 200 tests sont nécessaires pour couvrir l’ensemble de l’appli. Par contre ça devient plus gênant s’il en faut 10000… Tout est souvent question de contexte et c’est pour cette raison que nous sommes souvent en désaccord sur ce qu’il faut appliquer sur un projet. Tout le monde n’a pas le même parcours et n’a pas connu les mêmes problèmes. Il faut être capable d’estimer le contexte du projet courant et d’appliquer ce qui est nécessaire et pas plus.

Ajouter un commentaire