Se simplifier les tests Python avec Pytest


Personne n’aime faire des tests unitaires. C’est un peu comme les impôts: on sait que c’est utile, mais on est jamais content de s’en occuper.

Réchèr m’a dernièrement posé la question de l’abondance des méthodes assertTruc() et leur utilité, et je lui ai répondu que chaque méthode donnait des infos adaptées au test effectué.

Max m’a dernièrement fait la remarque que les tests “c’est bien mais c’est compliqué”. J’avoue être à court de contre-argument.

Et si on pouvait rendre les tests plus simples à écrire et à lire, aussi simples qu’un assert, mais avec un résultat plus clair que unittest en sortie ?

pip install pytest

Pytest est une lib de test à utiliser à la place de unittest. Ses créateurs utilisent l’introspection et l’injection de dépendance pour créer des tests magiquement.

D’ordinaire, la magie, on aime pas trop ça en Python, et on laisse ça aux rubistes. Mais dans le domaine du test, qui n’est pas un code de production avec les mêmes contraintes de lecture, de recherche de bugs architecturaux et d’interactions entre devs, mais qui a par contre une forte contrainte “j’ai pas envie d’écrire un caractère de plus”, ça a du sens.

Voilà comment ça se passe: on vire tout ce qui est classe et setup verbeux. On laisse juste les imports de vos libs, et les tests. Avec des assert. Pytest va alors analyser tout ça, et faire tout le boulot autour pour vous.

Exemple:

Dans votre lib:

def ma_fonction_a_tester(a, b):
    return a + b

Dans votre fichier test.py:

from malib import ma_fonction_a_tester
 
def test_function():
    assert ma_fonction_a_tester(1, 1) == 2

Et on lance :

py.test test.py

Pour obtenir:

====== test session starts ======
platform linux2 -- Python 2.7.3 -- pytest-2.3.2
collected 1 items

Bureau/test.py .

====== 1 passed in 0.02 seconds ======

Et voilà, les tests redeviennent bêtes et simples. Mais ils ne perdent pas en puissance. Car Pytest analyse le assert, et le transforme à la volée. Du coup, pour les structures de données complexes, Pytest va vous sortir les infos de debug utiles que assertTruc() de unittest vous aurait sorties.

Exemple avec des tuples:

def ma_fonction_a_tester(a, b):
    return (a * 2, b * 2)
 
 
def test_function():
    assert ma_fonction_a_tester(1, 1) == (2, 2, 3)

Va donner:

====== test session starts ======
platform linux2 -- Python 2.7.3 -- pytest-2.3.2
collected 1 items

Bureau/test.py F

====== FAILURES ======
______ test_function ______

    def test_function():
>       assert ma_fonction_a_tester(1, 1) == (2, 2, 3)
E       assert (2, 2) == (2, 2, 3)
E         Right contains more items, first extra item: 3

Bureau/test.py:7: AssertionError
====== 1 failed in 0.02 seconds ======

On nous indique clairement qu’il y a un item de trop dans mon résultat, et lequel.

En prime, Pytest nous affranchit des fonctions setUp() et tearDown() génériques. Le problème de ces méthodes dans unittest, c’est qu’elles sont exécutées à chaque début de test. On en a pas forcément besoin, et on a pas les mêmes besoins pour chaque test.

Pytest ajoute encore un peu de magie pour régler le problème:

Dans votre lib, vous avez ça:

import re
 
def extraire_title(html):
    """
        Extrait le title d'une page HTML à base de regex. C'est mal.
    """
    try:
        return re.search(r'<title[^>]*>(.*)</title>', html).groups()[0]
    except IndexError, AttributeError:
        return None

Dans votre fichier de tests, vous aurez:

import urllib2
 
import pytest
 
@pytest.fixture
def exemple_html():
    return urllib2.urlopen('http://www.google.com').read()
 
def test_extraire_title(exemple_html):
    assert extraire_title(exemple_html) == 'Google'

Qu’est-ce qui va se passer ?

exemple_html() va être déclarée comme une “fixture”, c’est à dire quelque chose qui contient ou génère des données de tests.

Quand Pytest va lancer les tests, il va voir qu’un argument de test_extraire_title() porte le même nom que la fonction exemple_html. Alors, il va automatiquement appeler exemple_html(), et passer le résultat à test_extraire_title() pour lancer le test.

On peut donc avoir des tas de fonctions de setup, partagées entre plein de fonctions de tests.

13 thoughts on “Se simplifier les tests Python avec Pytest

  • Oyo

    Génial, et c’est déjà intégré à Django:

    ici

    Top votre site, surtout continuez.

  • Soli

    Il existe toujours une notion de setup/teardown (à la xunit) dans py.test, qu’on peut établir au niveau d’un module, d’une classe, d’une méthode, etc. ça facilite la transition.

    Py.test peut aussi remplacer nose comme “test runner”/”test discoverer”.

    Il existe enfin de nombreux plugins, pour joindre aux tests unitaires des tests de couverture (via coverage de Ned Batcheler), distribuer ceux-ci (xdist, qui fait aussi du looponfailing), etc.

    Globalement, ce que je trouve remarquable c’est la flexibilité de py.test (notamment via la paramétrisabilité des tests, mais aussi la configurabilité).

  • Sam Post author

    Ce que j’aime avec les lecteurs de ce blog, c’est que globalement les commentaires sont d’excellente qualité.

  • Kontre

    “Max m’a dernièrement”, il manque l’apostrophe.

    Moi j’utilise doctest, ça suffit largement pour mes besoins persos (c’est plutôt du code de recherche). Et je me suis aperçu que je faisais parfois du TDD de manière naturelle avec, ça m’aide d’avoir un exemple sous les yeux pour me focaliser sur ce que je dois coder. Mettre en place tout un système de test complet, ça m’a toujours paru tellement fastidieux !

  • Sam Post author

    (juste pour le plaisir, moi je n’avais pas encore fais mumuse avec)

    Sinon oui, quand on a justes quelques fonctions qui prennent des primitives en paramètres, et qui retournent des primitives, doctest est vraiment idéal.

  • JeromeJ

    Hello, les fixtures ne sont appellées qu’une fois c’est ça ?

    Sinon merci bien car unittest c’est une pilule douloureuse dont on se passe souvent par conséquent :D

  • Sam Post author

    Les fixtures sont appelées une fois par test dont le nom de la fixture est en paramètre. Comme le précise soli, on peut aussi faire des fixtures qui sont appelées une seule fois en tout, mais je ne le montre pas dans l’article. Pytest est une lib très riche.

  • karl

    Pour ce qui est de exemple_html() il est souvent mieux de ne pas introduire de dépendance sur un paramètre incontrôlable. Dans ce cas, le fait que la connexion vers Google se fasse correctement. Il est ainsi mieux de fournir une chaîne de markup ou des chaînes d’ailleurs. Afin de tester les nombreuses possibilités commme <title\n>blah</title> ou <title>bli\nboulga</title> etc.

  • Sam Post author

    @karl: cet article ne s’intitule pas “apprenez les tests unittaires” ou “les bonnes pratiques des tests unittaires”. Le but est ici de comprendre pytest, et chaque information ajoutée qui n’est pas indispensable éloigne de cet objectif.

  • Sam Post author

    Nose ne s’occupe que de lancer les tests. Pytest permet cette syntaxe à base de assert.

  • Réchèr

    Je suggère que cet article ayant pour sujet principal “pytest” soit attribué du tag “pytest” (tag existant déjà pour d’autres articles).

    Je suis en train de m’y mettre (à pytest), je me souvenais qu’il y avait ce petit article d’intro et j’ai mis un peu de temps à le retrouver. Même Google a eu un peu de mal.

Comments are closed.

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