assert – Sam & Max http://sametmax.com Du code, du cul Wed, 30 Oct 2019 15:34:04 +0000 en-US hourly 1 https://wordpress.org/?v=4.9.7 32490438 Programmation par contrat avec assert http://sametmax.com/programmation-par-contrat-avec-assert/ Sat, 08 Sep 2018 19:40:11 +0000 http://sametmax.com/?p=24956 Le mot clé assert est populaire en Python essentiellement grâce à la lib pytest, dont on vous a parlé dans le dossier sur les tests unitaires.

En dehors de ce cas d’usage, personne ne comprend bien son utilité.

Déjà, dans les tutoriaux, on vous signale de ne pas l’utiliser pour faire des vérifications importantes, à cause d’une particularité d’assert: il peut disparaitre à tout moment !

En effet, si vous lancez Python toutes voiles dehors avec l’option -o (pour “optimize”), les lignes contenant ce mot clé sont ignorées.

Alors, mille millions de mille sabords, pourquoi se faire chier à avoir ajouté ce truc ?

A quoi un machin qui peut disparaitre à tout instant peut-il bien servir ?

Well, it’s not a bug, it’s a feature, my dear.

Voyez-vous, on connait souvent le premier effet kisskool d’assert:

    assert condition

Et si la condition est fausse, on se tape une exception AssertionError.

En revanche, ce que les gens savent moins, c’est qu’il existe une seconde forme, beaucoup plus utile:

    assert condition, "Message en cas d'erreur"

Qui fait tout pareil, mais permet de donner un feedback a qui tombe sur l’erreur susnommée.

Et c’est là que c’est intéressant: parce que ça vous permet de mettre des vérifications complexes dans le code, qui vont permettre aux devs d’éviter les âneries. C’est ce qu’on appelle un contrat.

Imaginez une fonction qui prend en paramètre une valeur, qui est la clé d’un dictionnaire:

    ARTIBUSES = {
        'truc': 1,
        'bidule': 1,
        'machin': '2',
        'chose': '3',
        'chouette': '3',
        'foo': '4',
        # ...
    }
    
    def souquer_les_artibuses(artibuse):
    
        target = ARTIBUSES[artibuse]
        # un code de souquage professionnel s'ensuit

Si quelqu’un utilise votre code, et insère la mauvaise artibuse (le vil flibustier !), notre fonction va (s’)échouer sur une KeyError. Ceci va obliger le contrevenant (le perfide faquin !) à regarder dans le code source et comprendre le code pour trouver la source de son désarroi. Quelle perte de temps pour notre dev (le fils de pute !).

Une solution est alors de mettre:

    def souquer_les_artibuses(artibuse):
        if artibuse not in ARTIBUSES:
            raise ValueError(
                f"'{artibuse}'' n'est pas une artibuse valide. "
                f"Utilisez une valeur parmi: {', '.join(ARTIBUSES)}"
            )
        target = ARTIBUSES[artibuse]

Et c’est certes une bonne solution, claire et explicite.

Mais elle a un défaut majeur: à chaque appel, on rajoute le poids d’un test qui n’a aucun intérêt pour le fonctionnement normal du programme. En fait, la majorité des appels de cette fonction se feront avec les bonnes valeurs (logique sinon le code planterait), et donc notre test est un poids superflu.

Ce problème se cumule quand les tests deviennent plus nombreux, plus complexes, et plus lourds, tandis que la fonction est appelée de plus en plus de fois.

Comment concilier donc son besoin impérieux d’aider son prochain, qui est probablement soi-même un vendredi à 3h du mat après un push qu’on n’aurait pas du faire en fin de semaine, et éviter ce gâchis de ressource ?

Ventre-saint-gris de sa race ! Avec assert bien entendu !


    def souquer_les_artibuses(artibuse):

        assert artibuse in ARTIBUSES, (
            f"'{artibuse}'' n'est pas une artibuse valide. "
            f"Utilisez une valeur parmi: {', '.join(ARTIBUSES)}}"
        )

        target = ARTIBUSES[artibuse]

En dev, ou quand on debug en production, on obtient toutes les vérifications et infos nécessaires. Le contrat à remplir avec notre fonction est vérifié. On peut mettre autant de clauses à notre contrat qu’on le souhaite, aussi lourdes qu’on veut !

Quand on est prêt à relancer la prod en mode propre, on exécute tout avec -o

Les vérifications faites en amont d’une fonction sont ce qu’on appelle des pré-conditions dans le jargon de la programmation par contrat. Ce sont les plus faciles à comprendre et les plus courantes. On peut néanmoins faire également des vérifications à la fin de la fonction, qui permette de vérifier qu’on est bien dans un état cohérent à la sortie de celle-ci.

C’est ce qu’on appelle des post-conditions: c’est moins courant, et moins facile à écrire, mais très puissant car on se souci d’un état final (est-ce que mon résultat est > 3 ?), et non de comment on y arrive. Un moyen bien plus souple d’attraper des erreurs plutôt que de chercher toutes les combinaisons de ce qu’on peut mal faire.

Effets secondaires de -o

Le saviez-vous ? Python vient aussi avec une variable magique, __debug__, qui est à True par défaut. Utiliser -o met cette variable a False. Plus fort encore, toute condition sur __debug__ sera retirée du code !

if __debug__:
    un_dump_log_de_porc()

Pouf, avec -o, plus de dump.

Il existe même -oo pour retirer en plus les docstrings, et économiser un peu de mémoire.

Le problème avec assert

Le problème avec assert, c’est vous. Enfin je dis vous, le vous du passé, celui qui ne savait pas. En effet, plein de devs vont utiliser des assert dans leur code sans penser à mal. Et vous arrivez avec votre -o, et boom, le code est tout cassé. C’est bien con de ne pas pouvoir stripper ses assert à soi sous prétexte qu’une dépendance est codée par un connard Bachi-bouzouk.

Or assert est très souple, et permet de tester littéralement n’importe quoi. Du coup, cela demande un peu d’expérience pour savoir quand l’utiliser, et quand ne pas le faire.

Le concept général est: si votre erreur concerne la fonctionnalité fondamentale de votre fonction, levez toujours une exception. Si en revanche vous testez si les valeurs des paramètres correspondent à quelque chose de sain, et qu’une erreur n’aurait pas d’effet de bord, vous pouvez (ce n’est pas du tout obligatoire, une exception normale reste un usage correct) utiliser assert.

Donc, n’utilisez pas assert pour tester la logique de votre programme, ni pour protéger l’intégrité de vos données face à une erreur.

Une dernière chose: n’utilisez pas assert pour fournir un contrat basé sur les types (comme par exemple, appeler isinstance()), puisque mypy et les types hints le font déjà et qu’ils sont maintenant très agréables à utiliser.

]]>
24956
Un gros guide bien gras sur les tests unitaires en Python, partie 1 http://sametmax.com/un-gros-guide-bien-gras-sur-les-tests-unitaires-en-python-partie-1/ http://sametmax.com/un-gros-guide-bien-gras-sur-les-tests-unitaires-en-python-partie-1/#comments Wed, 15 Jan 2014 15:26:00 +0000 http://sametmax.com/?p=8764 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.

]]>
http://sametmax.com/un-gros-guide-bien-gras-sur-les-tests-unitaires-en-python-partie-1/feed/ 21 8764