De la validation aux tests
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 à la compréhension de ce que sont les tests et la démarche ne semble 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.
Un gestionnaire de compos…
Il me faut bien sujet sur lequel travailler et pour rester dans le thème du blog, je vais faire un gestionnaire de compos.
Lors des tournois des jeux de stratégie/figurines, il faut préparer une compo, c’est à dire la liste de ce que l’on va jouer. Oui, c’est un peu comme la liste de l’équipe pour le foot. En anglais, on appelle cela un roster.
Une liste contiendra donc un nom et des éléments. Pour cet exemple, la première règle de gestion est qu’une liste ne peut avoir de doublon. Un élément sera donc représenté par une chaine de caractère. La seconde est que les chaines de caractères (éléments et titre) devront avoir la première lettre en capitale et le reste en minuscules.
Je peux ainsi créer un objet avec deux attributs (le nom de la liste et le conteneur effectif de la liste) et une méthode permettant d’ajouter un élément. Cela peut se représenter par cette classe.
class Roster: def __init__(self, name: str): self.name = name.upper() self._units = [] def add(self, unit): if unit in self._units: raise ValueError("Duplicate unit [{}]".format(unit.name)) self._units.append(unit)
Évidemment, dans l’état, cet objet est inutilisable : il manque dans cette classe l’accès aux informations liées aux éléments. C’est volontaire de ma part afin de me focaliser sur le code qui servira d’illustration aux tests.
Validons que cela fonctionne
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 :
if __name__ == '__main__': my_roster = Roster("Ultramarines") print(my_roster.name) my_roster.add("Scouts") print(len(my_roster._units)) try: my_roster.add("Scouts") except ValueError: print("Ok")
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, vous validez que le comportement correspond à ce que vous attendiez. En d’autres termes, vous testez déjà.
Finalement, qu’est-ce qu’un test ?
Ce code de test, vous l’exécutez afin de valider 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 validations
Automatiser des tests signifie écrire du code qui fera de manière automatique ce que nous faisons manuellement. Dans ce but, n’y a-t-il pas un moyen de faciliter cette étape ? Après tout, si nous avons une valeur attendue, pourquoi ne pas écrire simplement un test que cette valeur correspond à celle obtenue ? On peut donc faire évoluer le code précédent en :
if __name__ == '__main__': my_roster = Roster("Ultramarines") print(my_roster.name == "Ultramarines") my_roster.add("Scouts") print(len(my_roster._units) == 1) try: my_roster.add("Scouts") except ValueError: print("Ok")
Si nous exécutons notre code, nous avons l’affichage d’une série de True et un Ok. Dans ce cas, nous savons sans effort que tout se passe bien. Mais lorsque quelque chose n’est pas conforme, nous aurons certes un False, mais… nous n’avons aucune idée de sa provenance. Il faudrait donc améliorer ce point.
De la validation à l’assertion
Ce que nous avons écrit dans nos « tests » sous forme de code porte un nom, il s’agit d’assertions. D’après le Larousse, une assertion est une « proposition, de forme affirmative ou négative, qu’on avance et qu’on donne comme vraie« . Ici, nous affirmons que le nom de la liste est Ultramarines ou qu’après avoir ajouté un élément, la longueur de la liste est de 1.
Dans quasiment tous les langages, il existe une instruction assert qui permet de faire une assertion. Nous pouvons donc modifier notre code de la manière suivante :
if __name__ == '__main__': my_roster = Roster("Ultramarines") assert my_roster.name == "Ultramarines" my_roster.add("Scouts") assert len(my_roster._units) == 1
Oui, pour l’instant, j’ai mis la levée d’exception de coté.
Si nous exécutons ce code… Il n’y a plus d’affichage. Ce qui est intéressant, c’est de voir ce qui se passe lorsque l’assertion est fausse. Il y a une levée d’exception. Les évolutions positives sont que lorsque tout se passe bien, nous n’avons rien à analyser et lorsque quelque chose se passe mal, nous savons où. Mais le défaut, c’est qu’à la première exception, tout s’arrête.
C’est un bon début. On peut peut-être arranger un peu ce code.
Dans un module dédié
Déplaçons donc ce code de validation dans un fichier dédié. En passant, vous voyez que nous n’aurons plus besoin du main du module.
Dans ce module dédié, nous allons aussi confiner chaque validation dans une fonction dédiée. Dans ce module, notre code pourra ressembler à ça :
from tournaments_manager import models as mod def test_roster_name(): my_roster = mod.Roster("Ultramarines") assert my_roster.name == "Ultramarines" def test_roster_size(): my_roster = mod.Roster("Ultramarines") my_roster.add("Scouts") assert len(my_roster._units) == 1
Et bien voilà… Si, sérieusement, il n’y a pas besoin de plus pour faire passer votre code de validation à une automatisation de tests. Ce qu’il sera nécessaire, c’est d’ajouter une dépendance, pytest. En effet, si Python propose dans son installation standard un module de tests (unittest, un module similaire aux modules de test des autres langages), il reste pénible à mettre en œuvre. Pytest vise à simplifier l’écriture des tests.
Ainsi, après le classique
pip install pytest
Il ne nous reste plus qu’à exécuter l’instruction suivante dans un terminal
py.test tests/test_models.py
Et vous devez voir un message comme quoi tout s’est bien passé. Nous allons même pouvoir ajouter une fonction pour tester la levée d’une exception. Pour cela, pytest fourni un ContextManager :
def test_no_duplicates(): my_roster = mod.Roster("Ultramarines") my_roster.add("Scouts") with pytest.raises(ValueError): my_roster.add("Scouts")
Le plus intéressant est évidemment lorsqu’un test ne passe pas. Je vous laisse modifier votre code pour basculer dans ce cas. Vous voyez que vous pouvez facilement identifier l’erreur.
Quand même, ça en fait du code en plus…
C’est vrai, nous avons ici 14 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 fonctions, mais regardez bien : chaque fonction porte le nom du test qu’elle réalise. Chaque fonction 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 fonction 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 est suffisant pour un petit programme. Mais il ne s’agit que d’une première illustration. Vous voyez que ça peut être simple, alors n’hésitez pas à passer un peu de temps sur l’écriture de ce type de validation pour éviter d’en perdre beaucoup manuellement.
À 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.
4 réponses
[…] Mon premier billet sur les tests présente le concept et comment, avec peu d’effort, vous pouvez améliorer ce que vous faites déjà. Mais en conclusion, je vous affirme que ce premier jet de tests n’est pas une bonne pratique. Il y a plusieurs raisons et dans ce billet, je vais aborder la question de quoi tester. En particulier pour le langage Python qui a une manière particulière de gérer la visibilité des attributs. […]
[…] Mon premier billet sur les tests présente le concept et comment, avec peu d’effort, vous pouvez améliorer ce que vous faites déjà. Mais en conclusion, je vous affirme que ce premier jet de tests n’est pas une bonne pratique. Il y a plusieurs raisons et dans ce billet, je vais aborder la question de quoi tester. En particulier pour le langage Python qui a une manière particulière de gérer la visibilité des attributs. […]
[…] 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 […]
[…] 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 […]