Organiser ses tests unitaires
Après avoir essayé de démystifier les tests dans le premier billet de cette série, je vous propose de commencer à organiser votre code de test. Oui, oui, il faut l’organiser un minimum.
Vous avez vu qu’un premier principe de l’organisation des tests est de séparer le code de test du code fonctionnel. Ça, ça va aller, c’est facile à mettre en œuvre. Mais organiser son code de test signifie aussi savoir quoi mettre dans un test. Pour cela, je vais commencer par vous présenter ce concept de test unitaire. Ensuite, il faudra avoir un aperçu des frameworks plus standard que pytest.
Principe des tests unitaires
Il est en effet temps de les définir. Dans la grande galaxie des tests, les tests que nous sommes en train de mettre en place s’appellent des tests unitaires. Ils sont écrits par les développeurs (au sens large, c’est à dire n’importe quel auteur de code informatique) et destinés à s’assurer du bon fonctionnement du code produit. Ils se concentrent donc sur une petite partie du programme.
Je reviendrai sur ce point (dans un autre post), mais la notion unitaire de tests unitaires concerne bien cette notion : chaque test doit valider une partie limitée du programme et pour cela, le code à tester doit être le plus isolé possible de son environnement. L’environnement étant aussi bien le système (OS, réseau, ressource extérieur) que d’autres composants de votre programme. L’objectif étant d’une part d’avoir des tests déterministes, c’est à dire qu’à chaque exécution, ils doivent produire le même résultat, et d’autre part, ils doivent permettre d’identifier de manière précise toute défaillance.
Dans notre exemple, la notion unitaire va être simple à mettre en place puisque nous testons un objet qui n’a pas de collaborateur et qui ne dépend pas du système. Néanmoins, pour ce simple exemple de gestion de listes, nous pouvons voir que nous devons tester plusieurs choses : la création de l’objet et la gestion des éléments ajoutés. Nous avons donc besoin de valider deux fonctionnalités.
Des tests pour une fonctionnalité
Nous souhaitons tester le bon fonctionnement d’une fonctionnalité. Tester signifie manipuler de manière maîtrisée son système et s’assurer à un moment qu’il est dans l’état attendu. Mais chaque fonctionnalité n’a pas qu’un comportement unique. L’ajout d’un élément par exemple a deux comportements différents en fonction de l’ajout d’un nouvel élément éligible ou d’un élément unique en doublon. Tester une fonctionnalité nécessite donc toujours plusieurs tests.
Avec le principe des tests unitaires, chaque test validera un comportement. Ceci va nous conduire à une première règle
Nommez un test pour ce qu’il valide
Lorsqu’un test sera en échec, le framework de test vous dira quelle fonction est en échec. Nommée correctement, vous saurez immédiatement quel cas de quelle fonctionnalité est en échec. Vous saurez donc rapidement ce que vous avez à faire.
Mais on peut faire mieux.
Grouper les tests en modules
L’étape suivante consistera à grouper les tests pour s’y retrouver. Et cela commence par les répartir en modules et packages. Les tests doivent être dans une arborescence dédiée au sein de votre projet, en général dans un package racine tests.
Nous avons donc ici un second niveau d’identification du comportement non conforme. Les tests d’une même fonctionnalité peuvent donc être regroupés dans un même module. En cas d’échec, pytest vous informera quel module contient un test en échec. Vous pourrez ainsi identifier la fonctionnalité en régression.
Il y a tout de même un défaut : le reporting pytest met en évidence le nom de la fonction en échec. L’arborescence est plus discrète. Il y a tout de même une manière d’afficher un second niveau d’information.
Grouper les tests dans des classes
Les bibliothèques de tests unitaires modernes héritent toutes du canevas XUnit. Ceux-ci sont issus de langages objets tel que Smalltalk ou Java. En Java en particulier, il n’est pas possible de déclarer des fonctions. Les tests sont donc des méthodes de classes.
La bibliothèque Python de référence, unittest, utilise le même principe. Pytest s’en est affranchi mais laisse la possibilité de suivre le même patron d’écriture.
Ainsi, il est possible de grouper des méthodes dans une classe de test. Cette classe n’apporte dans la pratique que son nom, mais ce sera une information utilise pour le test en échec car le nom de cette classe devient le namespace du test.
Nos tests jusqu’ici en fonction pourraient devenir :
class TestAppendingUnit: def test_roster_size(self): my_roster = mod.Roster("Ultramarines") my_roster.add("Scouts") assert 1 == len(my_roster) def test_no_duplicates(self): my_roster = mod.Roster("Ultramarines") my_roster.add("Scouts") with pytest.raises(ValueError): my_roster.add("Scouts")
Ainsi, pour ceux qui sont habitués aux bibliothèques de test de type XUnit, vous pouvez conserver votre organisation. Cette organisation des tests a également un intérêt pour la génération du reporting.
À l’opposé, si vous n’êtes pas habitués à la création de classes, ce dernier exemple de code doit vous paraître peu intuitif. Pytest vous permet de n’écrire que des fonctions, vous pouvez donc choisir la manière la plus adaptée pour vous pour organiser vos tests.
Notion d’unité fonctionnelle
Nous avons vu dans le billet précédent que nous devons tester des fonctionnalités, pas des implémentations. Ceci est également vrai pour la notion d’unité à tester en ce qui concerne l’objet. En effet, pour pouvoir avoir la granularité la plus fine, nous pouvons être tenté de considérer que pour un objet, une fonctionnalité à tester correspond à une méthode. Mais dans notre cas, pour valider l’action d’ajout, il faut observer l’état. Cette observation passe par une autre méthode (ici len certes appelée indirectement). Une fonctionnalité est donc testée dans son ensemble.
Vous voyez qu’avec une bonne organisation, les tests peuvent vous faciliter la maintenance de votre code et vous épargner des heures sur un débuggeur.
À propos de... Darko Stankovski
iT guy, photographe et papa 3.0, je vous fais partager mon expérience et découvertes dans ces domaines. Vous pouvez me suivre sur les liens ci-dessous.