Avez-vous déjà été sûr à 100 % des horaires d’ouverture d’un restaurant précis ? Vous y êtes allé des millions de fois. Puis, un jour férié, vous avez faim et vous voulez aller y manger, mais vous vous apercevez soudainement que vous ne savez pas s’il est ouvert ou non ! 😦
L’écriture d’un test pour les cas qui sortent de l’ordinaire permet d'anticiper des situations exceptionnelles. Ainsi, vous éviterez d’être surpris par des échecs inattendus après la mise en service ! Vous vous souvenez de l'exemple de la fusée Ariane du premier chapitre ? 😛
Vous voulez disposer de tests unitaires de qualité, n’est-ce pas ? Cela implique de prendre en compte une gamme de situations inhabituelles dans lesquelles votre application pourrait être utilisée, et de les tester. Comment les envisager ? Heureusement, les testeurs qui nous ont précédés y ont pensé, et il existe de nombreux types de scénarios inattendus différents à prendre en considération quand on écrit des tests. Creusons la question !
Tirez parti des cas limites pour les scénarios alternatifs
Les cas limites (ou « edge cases » en anglais) sont des cas de test conçus pour gérer l’inattendu à la limite de votre système et de vos limites de données.
Quelle est la limite de notre système ? Et pourquoi parlons-nous de limites de données ?
C’est ce qui arrive lorsque les données avec lesquelles vous travaillez correspondent aux cas les plus extrêmes de vos règles métiers ou des types de données Java.
Cas limites dus aux règles métiers
Les règles métiers (ou règles de gestion) permettent de traduire le besoin du client pour un produit en exigence claire, que le développeur va pouvoir coder. Imaginez que vous travailliez pour une entreprise qui propose de la musique en streaming sur Internet. Pour pouvoir écouter de la musique sans interruptions publicitaires pénibles, vos utilisateurs auront peut-être besoin d’un abonnement payant pour le mois en cours. Aussi existe-t-il peut-être des quotas sur le nombre de chansons qui peuvent être écoutées simultanément. Ce sont des exemples de règles métiers.
Que se passerait-il si un utilisateur lançait une chanson longue de deux minutes, une minute avant l’expiration de son abonnement ? Que se passerait-il si deux chansons étaient lancées presque au même moment, l’utilisateur écouterait-il les deux ? Les règles métiers doivent répondre à ce genre de cas limite, et coder des tests pour ces exemples-là permet d'enrichir vos scénarios alternatifs.
Cas limites dus aux limitations techniques et physiques
En principe, votre système sait gérer les nombres entiers de base, le fameux type int ou Objet Integer en Java. Le plus grand nombre positif qu’un tel type peut contenir est environ 2,147,483,647. Ce nombre spécial est stocké dans Integer.MAX_VALUE
. Si vous développiez un calculateur, que se passerait-il d’après vous si vous ajoutiez 1 à cette valeur ? 1 de plus que le maximum ? Ce type de cas limite est une condition limite.
Le simple fait d’ajouter 1 au nombre le plus élevé peut, de façon contre-intuitive, donner un nombre négatif très bas. Vous ne me croyez pas ? Prouvons-le !
Pour voir ce qu’en pense Java, utilisez JShell. Voici un exemple de ce que vous pourriez voir. La valeur de la deuxième ligne après la ‘⇒’ est la réponse à Integer.MAX_VALUE +1.
jshell> Integer.MAX_VALUE $1 ==> 2147483647 jshell> Integer.MAX_VALUE + 1 $2 ==> -2147483648
Waouh ! C’est un nombre négatif très bas ! Je n’ai ajouté qu’1 ! Comment votre calculateur doit-il gérer cela ? Voilà une nouvelle question à poser au responsable de votre produit (le product owner si vous travaillez en pratique agile).
Vous pourriez utiliser le type Long, mais cela ne ferait que reporter le problème au nombre Long.MAX_VALUE (qui vaut certes la valeur respectable de 9 223 372 036 854 775 807 !). Java propose aussi un classe BigInteger, qui vous permet de vous affranchir de toute limite ! Magique, non ? Mais attention aux performances sur les très grands nombres ! C'est le prix de la magie. 😉
Voici un exemple qui fonctionne aux limites :
jshell> Integer.MAX_VALUE + 1 $1 ==> -2147483648 jshell> Long.MAX_VALUE + 1 $2 ==> -9223372036854775808 jshell> BigInteger.valueOf(Integer.MAX_VALUE).add(BigInteger.ONE) $3 ==> 2147483648 jshell> BigInteger.valueOf(Long.MAX_VALUE).add(BigInteger.ONE) $4 ==> 9223372036854775808
Avec BigInteger, la syntaxe est moins agréable, certes, mais cela peut répondre à la règle métier, et vous pouvez imaginer assez vite le test JUnit à coder !
Comment trouver de bons cas limites à tester ?
Voici de bonnes questions à poser lorsque l’on recherche des cas limites :
Quelle serait une valeur saugrenue à passer dans cette méthode, qui serait néanmoins légalement permise par son type défini ? Par exemple, passer une chaîne nulle, vide, ou incroyablement longue à Integer.parseInt(String) ?
Mon application fonctionnera-t-elle encore dans quelques années ? Est-ce que mes règles business autorisent des volumes de data croissants (par exemple d’en collecter davantage) ? Est-ce que j’ai des compteurs ou des chaînes qui vont évoluer jusqu’à poser problème par la suite ?
Si vous écrivez une méthode, demandez-vous : « Que pourrais-je essayer de faire pour la casser ? » Cela nécessite-t-il des arguments que je pourrais essayer de passer avec des valeurs inhabituelles ? Comme des valeurs nulles, des objets mal configurés, ou des chaînes vides, par exemple.
Dois-je coder avec un degré de précision défini pour mes cas de test (par exemple, exactitude à deux décimales près) ? Comment doit se comporter mon code s’il doit arrondir plusieurs fois ? Par exemple, diviser par deux et arrondir ; @Test diviser par deux et arrondir à nouveau. Serait-il toujours assez précis ?
Quand vous commencez à tester vos cas limites, souvenez-vous que s’ils échouent et qu’il n’y a pas de solution, vous pouvez trouver une façon élégante de gérer l’échec. Discutez-en toujours avec votre équipe et votre responsable de produit. Vous n’avez pas la prérogative de décider tout seul de ce qui va se passer. 🙂
Utilisez des cas pathologiques pour vos scénarios alternatifs
Avez-vous déjà passé une très mauvaise journée ? L’une de celles où tout semble s’être ligué contre vous ? Votre train a peut-être été retardé, ou vous étiez coincé dans les bouchons ? 🚧 Votre café du matin vous est peut-être tombé des mains tout seul ? ☕️ Même Internet est contre vous ! 📡
Votre produit doit fonctionner dans ce même monde réel, rempli d’imperfections et de problèmes. Quand vous publiez votre code, il sera exécuté sur n'importe quelle machine, loin de votre ordinateur. Des imprévus peuvent survenir. La bonne exécution de votre produit peut dépendre de la connectivité du réseau intranet ou Internet. Elle peut aussi dépendre indirectement du système d’exploitation sur lequel votre JVM fonctionne. Si vous y réfléchissez, il y a de nombreux éléments en jeu. De temps en temps, l’un d’entre eux rencontrera des problèmes, malgré tous vos efforts.
Les cas pathologiques (ou corner cases) ont lieu lorsque plusieurs cas limites se présentent ou se mélangent avec des dysfonctionnements extérieurs évoqués précédemment.
On ne peut pas toujours prévoir, mais vous pouvez poser des questions qui vous aideront à tester votre code pour des situations susceptibles d’affecter votre capacité à respecter vos engagements.
Il y a tellement de possibilités, par où commencer ? 😧
Bonne question ! Ci-dessous, vous trouverez quelques questions à poser lorsque l’on recherche des cas pathologiques. Pour rappel, tous les cas pathologiques ne sont pas à tester. Votre but est d’en identifier quelques-uns, puis de prioriser ceux que vous devez gérer. Voyez :
Qu’advient-il des données de mes utilisateurs si mon programme crashe ? 💥 Reste-t-il des données incomplètes quelque part ? Est-ce que cela pose problème ?
Mon code risque-t-il de violer certaines obligations légales ? 📃
Mon code risque-t-il de violer des promesses ou accords d'affaires ? 📊
Comment mon programme gère-t-il les problèmes externes ?
Et s’il y avait une panne du réseau imprévisible ?
Et si le serveur se trouvait à court d’espace disque ? 💾
Et s’il y avait un problème avec l’un des services dont nous dépendons ?
Que se passe-t-il si mon utilisateur fait deux fois la même chose ? 🔁
Mon programme se comportera-t-il comme prévu ?
Que doit-il faire ?
Si d’autres services dont je dépends (comme une base de données ou Google) deviennent lents, qu’advient-il du reste de mon logiciel ? ⏱
Vous vous poserez beaucoup d’autres questions en fonction de votre application. Il n’y a pas de règle immuable pour envisager les cas pathologiques, mais le brainstorming autour de différentes situations que votre code connaîtra est une façon de vous aider à trouver les questions à poser.
En résumé
Les tests de cas limites (« edge cases ») sont des tests conçus pour vérifier l’inattendu aux limites de votre système et de vos limites de données.
Les tests de cas pathologiques (« corner cases ») testent des situations improbables dans lesquelles votre application pourrait se retrouver.
Prenez en compte vos règles métiers et limites techniques pour vous guider dans la définition des cas de tests improbables.
Après avoir vu en détail comment tester de manière approfondie, il est temps de passer à une pratique encore un peu plus avancée des tests unitaires, en particulier par l'utilisation d'objets de simulation, ou "mocks" en anglais. Cela se passe... au chapitre suivant !