Un gros guide bien gras sur les tests unitaires en Python, partie 1


La zik maintenant traditionelle :

Les tests unitaires font partie de ces “bonnes pratiques” que tout le monde semble appliquer sur le net. Tous les devs hypes parlent de tests unitaires : les conférences, les blogs, les tutos, les livres, whooooo !

Dans la vraie vie vivante, on croise pourtant peu de gens qui les utilisent vraiment. On les retrouvent surtout dans les gros projets et les grosses boîtes, et encore.

Il y a plusieurs raisons à cela. D’une part, beaucoup, beaucoup, beaucoup de développeurs n’ont aucune idée de ce qu’est un test unitaire. Ceux qui savent, ne voient pas forcément l’intérêt, et ceux qui en voient l’intérêt n’ont pas forcément l’expérience nécessaire à leur mise en œuvre.

Je connais des tas de dev qui codent des tas d’excellents projets sans le moindre test unitaires.

L’adage selon lequel un code sans test unitaire est un code buggé est parfaitement faux puisque existe bien d’autres manières de tester son code. De plus, même un code bien testé est un code buggé. Je le sais, je l’ai codé.

Malgré cela, vous devriez maitriser l’usage des tests unitaires, car quand vous arrivez à vous sortir les extrémités digitales de la terminaison dorsale afin de les mettre en place, le bénéfice est très important. Mais aussi parce que certains projets ne peuvent pas s’en passer, et donc que vous ne pourrez pas travailler dessus sans savoir en faire. Certains projets sur Github n’acceptent pas de pull request sans couverture de tests, et certaines personnes n’utiliseront pas votre lib si elle n’est pas testée. C’est un gage de qualité.

Je n’en ferai pas une question morale ou de principe, les projets que l’on publie sur Sam et Max sont parfaitement exempt de tests unitaires, et d’ailleurs, la plupart des projets pros avec Max n’ont aucun tests non plus.

En revanche, en tant que freelance, je prends généralement le temps d’en faire.

Pas de dogmatisme du test donc, mais passé le goût de crabe dans la bouche, ça vaut le coup, alors lisez ce guide.

Qu’est-ce qu’un test unitaire

Le test unitaire est un bout de code qui fait exactement ce que son nom dit : il teste une unité de code.

Le problème c’est quoi tester, qu’est-ce qu’une “unité de code”, ce n’est pas quelque chose d’évident à définir, et vient avec la pratique. En théorie c’est un bout de code minimaliste, que l’on ne peut pas réduire plus. En pratique, on choisit avec pragmatisme un truc assez petit, mais pas trop, parce que merde, hein.

Mais alors que veut-on dire par “tester” ?

Et bien c’est d’une banalité affligeante : on donne des entrées au code, et on vérifie que ses sorties sont celles attendues pour ces entrées.

Bref, généralement (mais pas toujours) on teste une fonction. Souvent avec une autre fonction. Et c’est d’un manque d’originalité terrible.

Le test unitaire le plus bête qu’on puisse avoir en Python :

# Fichier de code
def fonction_a_tester(param1, param2):
    return param1 + param2
# Fichier de test
 
from fichier_de_code import fonction_a_tester
 
assert fonction_a_tester(1, 1) == 2  # test de l'addition
assert fonction_a_tester(1, -1) == 0 # test avec chiffre négatif
assert fonction_a_tester(4, 2) == 6 # test avec autre chose que des 1
assert fonction_a_tester(4.5, 2) == 6.5 # test avec des floats

Deux constats :

  • C’est parfaitement chiant. Les tests unitaires sont dans 99% des cas des tautologiques super ennuyeuses.
  • On teste le même code plusieurs fois, avec plusieurs cas de figure, pour être certain que ça se comporte comme prévu.

assert est un mot clé qui lève l’exception AssertionError quand l’expression évaluée ne retourne pas True. L’utilisation d’assert n’est pas le sujet de l’article, ici on s’en sert pour faire un test unitaire tout simplement parce que la première ligne qui ne renverra pas True fera planter le programme. C’est le test unitaire du pauvre.

Un test unitaire, ce n’est que ça. Un répétition bête et emmerdante de vérifications généralement très connes.

C’est minable ! A quoi ça sert ?

Là normalement vous vous dites “je sais ce que fait mon code, surtout une unité minimaliste, je n’ai pas besoin d’écrire des évidences pour le tester”. Et c’est pour cela que je ne suis pas dogmatique sur les tests unitaires, car c’est en partie vrai. Beaucoup de codes sont suffisamment simples ou peu critiques pour ne pas avoir besoin d’être renforcés par des tests unitaires. Et même si il faut des tests, tout le code n’a pas nécessairement besoin d’être testé.

Lancer un blog pour sa cousine n’est pas la même chose qu’une site de rencontre pour un grand compte.

Mais le test unitaire a plusieurs bénéfices. Le premier c’est qu’il vous oblige à réfléchir aux entrées et sorties de vos fonctions, et à l’API de votre code en général. Vous vous apercevrez à l’usage qu’un code est plus ou moins facile à tester selon la manière dont vous l’avez organisé, et ce faisant, vous serez forcé d’écrire un code plus souple, propre, extensible.

Écrire des tests fait de vous un meilleur développeur.

Cependant ce n’est pas le principal intérêt. Le véritable gain tient dans ce que vous gagnez dans le futur : quand vous allez modifier votre code, vous pourrez rapidement voir si il n’est pas cassé. En effet, votre code va grossir, et vous ne vous souviendrez pas de toutes les dépendances, de tous les effets de bords, de toutes les interactions. Certains dev sont meilleurs que d’autres à tout garder dans la tête, mais même Cortex a ses limites. Au bout d’un moment, le code est plus fort que vous.

À partir de là, vous allez tout de même avoir besoin de factoriser le code, bouger des choses, en ajouter d’autres, corriger un bug, faire un petit ajustement. À chaque fois que vous le faites, vous prenez le risque de casser un truc. Au début du projet, le risque est faible, et même si ça arrive, ça se répare vite. Après 2 mois de dev, les tests seront votre filet de sécurité. Vous pouvez les lancer après chaque modif, et voir que vous n’avez rien pété. Vous pouvez les lancer après une contribution d’un autre dev, et voir que ça tourne toujours. Vous pouvez les lancer après un changement d’environnement (OS, base de données, système de fichier, format, etc) et vous assurer que ça n’a pas d’impacts.

Particulièrement, des tests unitaires ont beaucoup de valeur sur un projet avec beaucoup de participants, tels que des logiciels libres populaires ou des systèmes de grandes sociétés.

Par exemple, sur notre dernière fonction bidon, on décide de faire une petite modification :

# Fichier de code
def fonction_a_tester(param1, param2):
    return int(param1) + int(param2)

On peut maintenant passer une string, et elle sera convertie en entier.

On lance notre batterie de tests, et là, au milieu de centaines d’autres tests, celui là foire :

assert fonction_a_tester(4.5, 2) == 6.5 # test avec des floats

On voit très vite que notre idée était pourrie, car on a un use case qui ne sera plus compatible. Si quelqu’un a utilisé des floats avec notre fonction, on va casser son code.

En l’essence, c’est ça l’intérêt des tests unitaires : vous faire sauter au yeux quand quelque chose casse. On appelle ça des “tests de régression”, et c’est l’usage le plus courant.

Plus tard vous verrez qu’on utilise aussi les tests pour développer son code (TDD), pour définir un comportement du produit avec le client (BDD) ou tout simplement pour servir de documentation.

Mais l’usage de base, c’est ça. S’assurer qu’on est pas en train de merder.

Résumé

  1. N’écoutez pas les Papes du test vous disant que si vous n’avez pas des tests unitaires à 50 ans, vous avez raté votre vie. Les tests, c’est bien. Un projet livré, c’est mieux. Une documentation est plus importante que des tests. Les 3, évidement, c’est l’idéal.
  2. Un test, c’est une suite parfaitement chiante d’énonciations d’évidences. Il n’y a généralement rien de compliqué dans les tests. Vous vous sentirez parfois insulté en les écrivant tellement c’est con.
  3. L’intérêt majeur des tests est d’avoir une alerte rouge qui se lance quand vous avez pété un truc. Ça arrive bien plus souvent que vous ne le croyez sans que vous ne vous en aperceviez car vous n’avez pas de tests.

Ces bases posées, la prochaine partie fera la démonstration du module unittest afin de créer vos premiers tests unitaires en Python, puis on enchaînera, partie par partie, sur les applications pratiques, les variantes, les girafes lesbiennes et tout ce qui fait un bon article de s&m.

Dans la partie 2, on va voir comment faire des tests en utilisant la lib standard Python.

21 thoughts on “Un gros guide bien gras sur les tests unitaires en Python, partie 1

  • bob

    Wai, j’ai bossé dans une boite de merde avec un projet tout pourri en code spaghetti, mais qui était le produit phare de la boite.

    Chaque mise en prod était infernale les clients trouvait toujours une régression résultat le temps de travail explosait: hop on rollback on corrige, on met en prod, un autre truc pète, on rollback …etc

    Quand on est passé aux tests ça a sauvé l’équipe de dev du suicide collectif, on mettait notre code de daube dans le plat de spaghettis => hop on teste => bonheur. D’ailleurs les tests c’était le seul code propre dans ce foutu projet.

  • Fred

    Bonjour

    On pourrait passer immédiatement aux girafes lesbiennes s’il vous plait ???

  • OKso

    Un peu de pytest pour la pratique ? C’est tellement plus agréable pour écrire des tests unitaires que les outils de base lubrifiés au gros sel.

  • roro

    Au test “unitaire”, je préfère le “crash test”; qui consiste à tenter de planter par tous les moyens possibles et imaginables.
    Et c’est beaucoup plus rigolo.

  • Casaubon

    L’intérêt du test unitaire, c’est pas de vérifier le cas bêtissime, mais le corner case.
    Même avec l’addition, il un en a :
    assert fonction_a_tester(10., 0.1) == 10.1
    assert fonction_a_tester(10., 1.0e-17) == 10.0/code>

  • Stéphane

    Notre expérience au bureau a été de faire de moins en moins de tests très unitaires avec plein de mocks pour aller vers des tests qui en font plus (plusieurs fonctions). Pour avoir une idée, avec django ça donnerait un test sur une fonction dans le module views et on vérifie les données enregistrées et la réponse retournée par la fonction.

    On a été poussé à faire des tests plus large (sans êter fonctionnel) parce que les mocks partout rendaient le code de tests long à écrire et le code peu modifiable facilement finalement.

    Peut-être qu’on avait mal compris qu’on était pas au bon niveau de finesse et qu’il nous a fallu plusieurs années pour comprendre…

  • C

    roro: Faire ça en groupe et se battre pour faire planter le coder de l’autre, c’est assez rigolo :)

  • calagan

    Je fais partie des gens qui ne font jamais de tests…en même temps je ne suis pas développeur de métier mais admin, donc nous les admins comme on se sent supérieur aux autres on fait pas de test. On aime pas les chefs, les managers en carton et de manière générale les patrons du MEDEF…

    Sinon en Python on fait comment, concrètement, pour tester une application web ? On simule le réseau ? Comment on fait pour tester des interfaces graphiques en opengl…j’ai déjà essayé, j’ai passé plus de temps à coder les tests que l’appli finale.

    Pour moi les tests, c’est comme les comptes-rendus et l’entretien annuel d’évaluation, c’est pour montrer à celui qui te paie que tu n’es pas une merde, tu dois constamment prouver ta fonction alors que c’est pour cette fonction que l’on t’a embauché…bref pour moi les tests c’est de la branlette de manager…

    Et au fait si c’est vous 2 qui vous occupez du site xhamster.com, est-ce que vous pouvez rajouter plus de mature et de big tits ? Merci, car je préfère quand mêmes les gros seins aux tests unitaires et ça comme dirait, l’autre grosse pédale à la voix d’eunuque de dance avec les starts : j’achète ;)

    • Max

      Et au fait si c’est vous 2 qui vous occupez du site xhamster.com, est-ce que vous pouvez rajouter plus de mature et de big tits ? Merci, car je préfère quand mêmes les gros seins aux tests unitaires et ça comme dirait, l’autre grosse pédale à la voix d’eunuque de dance avec les starts : j’achète ;)

      Si c’était le cas je serais au milieu des putes H24. J’aurais même déjà fait une overdose de biatchs…

  • Greizgh

    J’aime les girafes mais ce que je préfère c’est enculer les mouches:
    Le véritable gain tient dans ce que vous gagnerz dans le future.

    Mon grain de sel: les tests c’est bon, ça rassure quand on se lance sur une nouvelle techno et ça ne prend pas tellement de temps à écrire.

    J’adhère à l’idée que tout n’a pas besoin d’être testé: sur nos dev on ne teste que les parties les plus critiques de l’appli. (Faut quand même se garder des trucs à péter).

  • OKso

    @vaurien : tu as 2 niveaux de tests possibles : à l’intérieur de l’application (tests via le framework sans vraiment faire de requête HTTP) et à l’extérieur de l’application (codes HTTP 404/500, contenu des pages renvoyées, …)

    Le premier niveau dépend du framework web que tu utilises (Django, Bottle, Flask, …)

    Pour le second, il existe plein de “spiders web” pour parcourir ton site (wget par exemple), et une série de libs pour en plus tester le contenu. Pour aller plus loin, tu peux stress-test le réseau avec des outils comme Vilain [ http://vaurien.readthedocs.org/en/ ] qui va foutre un peu la merde dans tes connexions réseau.

  • Sam Post author

    Beaucoup de questions et de demandes ici, donc je vais tacher d’y répondre dans les parties futures plutôt que dans les commentaires, ce sera plus efficace. Si j’en oublies, vous savez comment nous hurlez dessus….

  • Syl

    Retour en force! Merci pour cet article!

    Petite boulette:
    “Le véritable gain tient dans ce que vous gagnerz dans le future

  • Baronsed

    Merci d’aborder le sujet. Vous participez à la formation de gens plus compétents.

    Je trouve néanmoins que ça manque de quelques trucs, c’est à dire que j’ai des questions (sur des points me semble-t-il plutôt concrets) :
    – existe-t-il une convention de nommage pour les tests, la structure des répertoires, les noms, etc. ? Et qu’en est-il de l’affichage des résultats ? Pour s’y retrouver avec d’autres dev.

    – à propos des “assert” : ça fait crasher à la première erreur, mais est-ce qu’il ne faudrait pas justement afficher tous les résultats des tests afin de ne pas risquer de sous-estimer la portée d’une erreur ?

    – dans la pratique, qu’est-ce qu’on utilise ? J’ai entendu parler de VM distantes lancées automatiquement pour tester une install… est-ce qu’on peut lancer les tests automatiquement avant un commit (githooks(5) ?) ?

    Ce qui relève plus de remarques :
    – à quoi penser quand on écrit les tests : pour bien tester, il faut connaître les limites techniques de ce qu’on manipule. Par exemple, s’il s’agit de réels, on va tester de très grandes valeurs, de très petites, 0, 1, des négatifs, et toutes les combinaisons entre ceux-ci s’il y a plusieurs champs
    – les test peuvent porter sur des tas de choses : précision, performance…

  • Sam Post author

    @Baronsed : c’est la partie 1 seulement. Le reste arrive.

  • Lujeni

    @calagan Tu as une vision bien dur des tests. La on parle de test unitaire mais je pense que dans ton cas tu pourrais plus avoir besoin de test fonctionnel. Plus simple à mettre en place et souvent plus rapide. Après la grande question, mes tests fonctionnels sont OK donc je n’ai pas besoin de tests unitaires ? :)

  • loicl

    Salut les amis, merci pour le tuto, par contre j’ai une incohérence par dessus le marché.

    “On lance notre batterie de tests, et là, au milieu de centaines d’autres tests, celui là foire :

    assert fonction_a_tester(4., 2) == 6. # test avec des floats

    On voit très vite que notre idée était pourrie, car on a un use case qui ne sera plus compatible. Si quelqu’un a utilisé des floats avec notre fonction, on va casser son code.”

    Chez moi ça ne foire pas, c’est normal ?

    ma conf :

    Python 2.7.6 (default, Mar 22 2014, 22:59:56)

    [GCC 4.8.2] on linux2

    ubuntu trusty

  • Toine

    Ce qui est pénible lorsque l’on écrit les tests unitaires, c’est les jeux de données à utiliser et il faut du coup écrire les différentes combinaisons (ce que ‘lon voit dans l’exemple).

    Je en sais pas si vous avez suivit mais il a été annoncé, il y a peu, la sortie d’hypothesis 1.10.1 (la doc est là ). Hypothesis permet par l’utilisation de décorateur de définir ces fameux jeux de données (par des décorateurs et la notion de trategy). Du coup, la partie pénible du test est supprimée ou presque;

  • Anon

    Bonjour.

    Fichier de test

    from fichier_de_code import fonction_a_test

    C’est fonction_a_tester (au cas où personne l’aurait vu)

Comments are closed.

Des questions Python sans rapport avec l'article ? Posez-les sur IndexError.