Des astuces avec pytest


Pytest est fantastique. En fait si la lib n’est pas dispo je n’ai même plus la volonté d’écrire des tests unitaires tellement je suis habitué aux facilités qu’elle offre.

Au fur et à mesure de son usage, j’ai noté quelques astuces qui, je le sais, vous serviront bien sur le long terme.

Choper une exception avec un message particulier

On peut vérifier qu’une fonction lève bien une exception avec la fonction raises():

import pytest
 
def test_truc():
    with pytest.raises(MachinError):
        foo()

Ce test réussira si foo() lève bien de manière consistante MachinError. Mais parfois on a plein d’erreurs similaires, et on en veut une avec un message particulier. Dans ce cas, on peut matcher le message sur une regex:

def test_truc():
    with pytest.raises(MachinError) as excinfo:
        foo()
     excinfo.match(r"votre regex du message d'erreur")

Configurez, il en restera toujours quelque chose

On peut mettre la config de votre projet dans un fichier pytest.ini (mais le fichier tox.ini ou setup.cfg marche aussi \o/). Parmi les options les plus pratiques:

addopts, qui permet de forcer des options à la ligne de commande pytest pour ne pas les passer à chaque fois à la main.

Celles que j’active toujours:

--exitfirst : le premier échec arrête les tests. Pas la peine de me mettre 22 mille erreurs.
--capture=no : affiche les print() de mon code.
--ignore="virtualenv" : permet de ne pas cherche les tests dans un dossier. Par exemple le virtualenv.
-vv : bien verbeux. Je veux de l’info sur mes tests.
--showlocals : montre les variables locales sur les tests qui foirent.

Par exemple:

[pytest]
addopts = --exitfirst --capture=no --ignore="virtualenv" -vv --showlocals

Mais bon comme j’ai toujours ces trucs-là activés, je force ces options dans mon .bashrc via la variable d’env:

export PYTEST_ADDOPTS="--exitfirst --capture=no -ignore="virtualenv" -vv --showlocals"

Autres options sympas:

testpaths qui permet de choisir les chemins dans lesquels chercher les tests:

Ex:

[pytest]
testpaths =
    tests
    foo
    bar

Si à l’intérieur de ces dossiers il a un truc à ne pas choper, norecursedirs est là pour ça :

[pytest]
norecursedirs =
    tests/media
    tests/scripts

Les options pratiques de tous les jours

Il y a des options qu’on ne veut pas tout le temps, mais qui sont super utiles ponctuellement:

--failed-first : relance tous les tests, mais ceux qui ont foiré en premier.
--pdb : lance pdb juste après le premier échec.
-k : lance uniquement les tests dont le nom matche cette regex.

Et souvenez-vous que vous pouvez lancer un seul test en spécifiant son chemin avec la syntaxe relative/file/path.py::test_func. Par exemple:

pytest test/test_foo.py::test_bar

Vive les plugins

Pytest a une grosse communauté de plugins, il suffit de chercher sur google “pytest + techno” pour avoir tout de suite des helpers qui popent.

Ceux que j’utilise très souvent:

pytest-pythonpath :

Permet d’avoir dans son conf file python_paths = chemins qui seront ajoutés au PYTHON PATH:

Ex:

[pytest]
python_paths =
    .
    libs

pytest-flake8 permet de lancer le linter flake8 pendant les tests et de considérer son échec comme un test qui foire. En plus flake8 peut mettre sa config dans les mêmes fichiers que pytest donc j’ai souvent ça:

[flake8]
exclude = doc,build,.tox,.git,__pycache__,build # ignorer les dossiers inutiles
max-complexity = 10 # verifier que le code n'est pas trop complexe
max-line-length = 80 # pep8 for ever, mais noqa peut servir parfois

Après j’utilise souvent tox, donc ce plugin n’est plus aussi utile qu’avant.

pytest-django qui ajoute des setup et tear down avec la base de données Django, fourni des fixtures pour le client de test django et permet de configurer DJANGO_SETTINGS_MODULE dans son fichier ini.

pytest-asyncio qui permet de gérer la loop, utiliser await dans les tests, etc.

Gérer ses fixtures

Le système de fixtures est une des choses qui rend pytest si cool, particulièrement parce qu’on peut être très précis dans ce qu’on charge. Mais des fois on veut les fixtures pour tout le monde, ou tout un module. autouse fait exactement ça:

@pytest.fixture(autouse=True, scope="module")
def ma_fixture():
    return foo()

Et cette fixture sera instanciée une seule fois pour tout le module. On peut aussi avoir un scope de session (une seule fois par lancement de pytest) ou de class (une fois par classe).

Si vous avez besoin de lancer un code avant toute session de tests, par exemple pour vous définir des fixtures qui sont dispo dans tous les tests, il suffit de le mettre dans un fichier conftest.py à la racine de vos tests. Ce fichier est automatiquement détecté par pytest, et lancé avant toute chose. Il permet également de faire des hooks complexes, créer ses propres plugins, etc. Mais je l’utilise surtout pour faire des fixtures globales et m’éviter de les importer.

11 thoughts on “Des astuces avec pytest

  • Oprax

    Tout d’abord un grand merci pour votre travail, votre blog m’es très utile (et divertissant !)

    J’ai une question : excinfo.match(r"votre regex du message d'erreur") ne manque pas un niveau d’indentation (pour être au même niveau que foo()) ?

    Encore merci pour l’article qui tombe bien =)

  • kontre

    Je suis en train de faire des tests unitaires en C. Je pleure… J’ai même pensé à faire une surcouche python pour faire les tests avec pytest, mais c’était vraiment overkill pour le coup.

    Pourquoi ne pas toujours mettre --failed-first ? Si tous les tests passent ça ne change pas grand chose ? Ou tu préfères garder les tests triés dans le cas général ?

  • Sam Post author

    J’ai jamais vraiment réfléchi à la question. Ca parait logique :)

  • Tryph

    “On peut aussi avoir un score de session” -> scope

    je vais pas en faire des caisses, mais comme c’est la première fois que je poste: merci pour le blog super utile ;)

  • rm_ass

    Le plugin pytest-cov est bien cool aussi, il permet de récupérer le pourcentage de code testé et aussi les blocks qui ne le sont pas !


  • Sam Post author

    Ouai d’ailleurs faut que j’ajoute l’usage de coverage.py à la série sur les tests.

  • toub

    Pour être sûr de comprendre autouse:

    la fixture est invoquée automatiquement à la condition qu’elle soit déclarée dans le conftest, c’est ça ? Ensuite le scope va moduler le nombre de fois ou elle sera invoquée (session, ou module, ou test) ? Ou alors c’est pas ça du tout ?

    Bien intéressé par les plugins de coverage également. Et est-ce qu’il y a des outils de tests par mutation associé à pytest ?

    Merci pour le post, en particulier le 1er sur pytest de l’année dernière, c’est top comme outil

    @kontre : pour tes tests en C, je recommande boost test, pas aussi puissant que pytest mais ca facilite largement les tests u, par contre ca implique de compiler ton code C avec g++ – ou alors des wrappers extern “C”.

  • touilleMan

    “addopts = –exitfirst –capture=no -ignore=”virtualenv” -vv –showlocals”

    Il manque un “-” à l’option “–ignore=…” ça m’a joué des tours en copiant-collant sans regarder !

  • Ryzz

    Dans ton exemple avec autouse, tu mets une valeur de retour à laquelle tu ne pourras accéder que si tu fait explicitement appel à la fixture, malgré l’autouse. Dans l’exemple ci-après, le premier test échoue avec un «NameError: global name ‘myfixture’ is not defined».

     
    import pytest
     
    def myfunc():
        return "y town"
     
    @pytest.fixture(name="myfixture", autouse=True)
    def fix_myfixture():
        return "myfixture value"
     
    def test_myfixture():
        assert myfunc() == "y town"
        assert myfixture == "myfixture value"
     
    def test_mymixture(myfixture):
        assert myfunc() == "y town"
        assert myfixture == "myfixture value"

    (J’ai mis un bloc code mais apparemment, il faut imaginer l’indentation…)

  • Sam Post author

    Autouse ne déclenche pas l’injection de la fixture automatiquement, ça exécute seulement le code dedans. C’est utile par exemple pour faire des opérations d’initialisation (créer des dossiers, lancer un serveur, etc) ou de néttoyage (supprimer les dossier, tuer le serveur, etc).

Comments are closed.

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