Une particularité du langage Python est qu’il ne se limite pas au monde des développeurs. Pour l’écrasante majorité de mes stagiaires, écrire un programme n’est pas leur activité principale mais doit les aider dans leur métier. Inutile de dire que tout ce qui a un rapport à la production de code doit aller à l’essentiel.

Du coup, il y a une partie qui provoque toujours la même réaction de lassitude : les tests. Cette réaction d’intérêt mitigé vient du fait qu’ils ne codent que des scripts à usage limité pour lesquels l’investissement d’une démarche de test n’est pas nécessaire. C’est certainement le cas aussi pour tous ceux qui n’écrivent pas du code à usage professionnel.

Vous vous doutez bien que si j’écris ce billet, c’est que la vision du test n’est pas tout à fait exacte et résulte d’une incompréhension de l’objectif du test dit de développeur. Je vous invite donc à lire le billet suivant afin d’essayer de comprendre ce qu’est un test, car vous en faites déjà… ou presque.

Je vais bien entendu illustrer mes propos par un exemple alors définissons le tout de suite.

Une gestion de tâches

Oui bon, classique mais qui assez simple pour aborder le sujet.

Globalement, je veux gérer une liste de tâches classées en thème. Un thème est évidemment un nom. L’ordre d’ajout des tâches doit être respecté. Une tâche sera représentée par un simple texte dont la première lettre sera en capitale et le reste en minuscules. Et ce sera la même règle pour le nom du thème. Cerise sur le gâteau, une tâche ne doit pas exister en double.

Pour cela, je peux créer un objet avec deux attributs (le nom du thème et la liste des tâches) et une méthode permettant d’ajouter une tâche. Cela peut se représenter par cette classe.

Évidemment, dans l’état, cet objet est inutilisable : il manque dans cette classe l’accès aux informations liées aux tâches. C’est volontaire de ma part afin de me focaliser sur le code qui servira d’illustration aux tests.

Tester, vous le faites déjà

Lorsque vous écrivez du code, vous l’exécutez pour voir ce que ça fait. Quand le programme est un peu long, vous ajoutez des print pour vérifier l’état du programme. En d’autres termes, à un module Python, vous ajoutez ce genre de code là :

Une fois le code exécuté, vous vérifiez visuellement que ce qui est affiché correspondent à ce qui est attendu.

Évidemment, en Python, vous pouvez faire ces tests dans un shell interactif afin de bien maîtriser et valider les comportements.

Dans les deux cas, sur le principe, vous testez déjà.

Finalement, qu’est-ce qu’un test ?

Ce code de test, vous l’exécutez afin de vérifier que le code fonctionne. Vérifier que le code fonctionne est la base du test.

Les print puis le bloc try/except suivant est du code qui sert à vérifier que le code fonctionne correctement, ou plutôt, comme attendu. Le print permet de vérifier que le code retourne bien la valeur attendue ou, dans le cas du try/except, que l’exception est bien levée lorsque vous tentez d’ajouter 2 fois la même tâche.

Le défaut de cette pratique

Le défaut de cette pratique est qu’elle mobilise l’humain pour valider le comportement. Vous êtes obligé de vérifier que la valeur correspond bien à celle attendue. Mais plus le code est compliqué, plus ces affichages sont incompréhensible, alors on se met à commenter des parties, à compléter l’affichage… Et on passe un temps non négligeable à analyser le résultat du test.

Aussi, lorsqu’on aborde la notion de tests, il s’agit avant tout de l’automatisation des tests.

Automatiser les tests

Automatiser des tests signifie écrire du code pour vérifier la bonne exécution du programme. Il faut donc exécuter votre programme et vérifier qu’il fait ce qui est attendu. La première partie, nous l’avons déjà écrite d’une certaine manière ci-dessus.

La seconde partie est pour l’instant manuelle. L’idée va donc être de traduire ce comportement en code. Pour mettre en œuvre la seconde partie, il existe des bibliothèques dédiées aux tests. Python propose une bibliothèque en standard : unittest.

Le principe consistera donc à écrire une classe de test (héritant de unittest.TestCase) contenant des méthodes de test. Le nom de ces méthodes doit commencer par test (elles sont exécutées par introspection). Chaque méthode contiendra du code de test puis de validation de son état.

Vérifier l’état attendu se fait par des méthodes assert qui permettent de déclarer une assertion. La documentation propose une liste des méthodes disponibles. Nous allons donc faire une ou plusieurs assertions sur l’état.

Essayons donc de transformer le code de validation précédente en tests automatisé.

Un premier test

Commençons par convertir en test la validation du nom associé aux tâches. Lorsque nous passons en paramètre une chaîne de caractères, celle-ci doit devenir l’attribut name avec la majuscule en capitale et le reste en minuscules.

Le code initial du module de test avec le test correspondant précédant sera donc le suivant :

Les deux dernières lignes vont vous permettre d’exécuter ce module et donc d’exécuter le test. Si tout se passe bien, vous avez simplement un message comme quoi tout s’est bien passé. C’est le principe des frameworks de test : ils ne vous insultent que si quelque chose se passe mal. Si tout se passe bien, vous aurez un ok avec un petit reporting.

Retenez que le principe des tests est d’identifier si quelque chose se passe mal, vous ne devez pas avoir de print ou autre instruction nécessitant une validation manuelle.

Écrivons les autres tests

Nous pouvons donc ajouter des méthodes de test pour nos autres validations.

Nous avons donc maintenant un code qui exécute la même chose que notre main précédent mais qui valide également que cette exécution a le comportement attendu. Sur le principe, vous n’avez plus à valider vous même l’exécution de chaque test.

Ça en fait du code en plus…

C’est vrai, nous avons ici 17 lignes au total dans un nouveau module contre 9 précédemment. Oui, écrire ces quelques lignes va prendre un peu de temps, mais en contrepartie, vous n’en perdez pas à valider manuellement le comportement. Bien entendu, nous aurions pu faire l’économie de la multiplication des méthodes, mais regardez bien : chaque méthode porte le nom du test qu’elle réalise. Chaque méthode déclare une intention, ceci est important car en cas d’échec, vous saurez quel test est en échec et saurez vers quoi vous diriger pour les corrections. En effet, imaginez ne faire qu’une méthode de test, si elle est en échec, c’est quoi ? L’affectation du nom ? L’ajout d’une tâche ? La présence de doublon ? Vous serez reparti à explorer manuellement l’ensemble des tests.

Le but des tests automatisés est de pouvoir identifier rapidement un test en échec, en conséquence, du code qui ne fonctionne plus comme attendu. Du code qui  fonctionnait et ne fonctionne plus, on appelle ça une régression.

On y est presque

Vous voyez que sur le principe, vous écrivez naturellement des tests. Ce qui vous manque est l’automatisation.

Bien entendu, avec du code plus complexe, il faut s’organiser un minimum. Le code de test que j’ai écris ici n’est pas une bonne pratique, il a juste comme but premier de vous illustrer l’usage.

Comment faire, ce sera pour les prochains billets.

À 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.