Après avoir essayé de démystifier les tests dans le premier billet de cette série, puis après vous avoir donné un aperçu de quoi tester, je vous propose de commencer à organiser votre code de test.

Vous avez vu qu’un premier principe de l’organisation des tests est de séparer le code de test du code fonctionnel. Ensuite, il s’agit de les répartir en différents modules et ces classes de type TestCase. Pour cela, je vais commencer par vous présenter ce concept de test unitaire.

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 tâche, nous pouvons voir que nous devons tester plusieurs choses : la création de l’objet et la gestion des tâches. Nous avons donc besoin de valider deux fonctionnalités et pour chaque fonctionnalité, il faut plusieurs tests. La création de l’objet par exemple mérite de tester la bonne initialisation des attributs, mais aussi le respect de la règle fonctionnelle de nommage.

Organisation des tests unitaires

Pour commencer, puisque nous testons ici une classe, nous allons les regrouper dans un même module. Dans ce module, nous allons grouper les tests par fonctionnalité testée et pour cela, nous allons créer une classe héritant de TestCase par fonctionnalité testée. Chaque classe regroupera tous les cas de test de cette fonctionnalité.

La bonne pratique est ici de bien nommer ces divers composants. Les classes doivent permettre d’identifier quelle fonctionnalité est testée et chaque méthode doit explicitement informer quel comportement est testé. Nous pouvons ainsi proposer les tests suivants pour l’instanciation des objets.

En fonction de la complexité de vos objets, vous pouvez évidemment avoir besoin de bien plus de tests.

Preparer un environnement de test

Tester l’initialisation d’un objet n’a, dans notre cas, aucun prérequis.

Tester l’ajout de tâches nécessite évidement l’existance d’un objet de type TaskManager. Nous pouvons évidemment créer cet objet au début de chaque test. Mais puisque nous allons tester plusieurs cas, il peut être judicieux de s’assurer de tester des objets ayant eu une initialisation uniforme. Pour celà, unittest propose la méthode setUp qui sera appelée avant chaque méthode de test. Nous avons également la méthode tearDown qui sera appelée après chaque méthode de test et qui a pour objectif de nettoyer l’environnement afin de s’assurer que chaque context d’executon soit identique.

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 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 pour valider l’action d’ajout, il faut observer l’état ce qui passe par une autre méthode (len certes appelée indirectement). Une fonctionnalité est donc testée dans son ensemble.

Nous allons ainsi pouvoir ajouter dans cette classe ces tests :

Bien entendu, en fonction de la complexité de votre code, vous aurez plus de cas de tests.

L’importance de la granularité et du nommage

Vous comprenez bien avec ces exemples que la structures en modules, classes de type TestCase et méthodes n’est qu’une structure arborescente vous permettant de ranger vos cas de tests. Lorsqu’un cas de test (une méthode) sera en échec, la classe également. Ceci doit donc vous diriger vers une structure où la classe représente la fonctionnalité testée et les méthodes, les cas testés.

Vous comprenez également qu’en allant vers une granularité faible, c’est à dire que vous testez une portion réduite de code, vous vous simplifierez l’identification du défaut de votre code.

En conséquence, plus que jamais, le choix des noms des classes et méthodes pourra vous simplifier la maintenance et la correction.

Ici, c’est d’autant plus important que :

  • chaque test déclare une intention de comportement. Ainsi, le nom du test exprime ce que vous souhaitez vérifier. À la lecture des noms des classes et des méthodes, le lecteur doit avoir une idée de ce qui a été testé et du comportement attendu.
  • si un test est en échec, unittest vous informera lequel (nom de classe et de méthode), ce qui doit vous faciliter l’identification de ce qui ne va pas et l’identification de l’intervention pour la correction.

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.