pytest – Sam & Max http://sametmax.com Du code, du cul Wed, 23 Dec 2020 13:35:02 +0000 en-US hourly 1 https://wordpress.org/?v=4.9.7 32490438 Des astuces avec pytest http://sametmax.com/des-astuces-avec-pytest/ http://sametmax.com/des-astuces-avec-pytest/#comments Tue, 30 Aug 2016 09:36:39 +0000 http://sametmax.com/?p=20037 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.]]> 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.

]]>
http://sametmax.com/des-astuces-avec-pytest/feed/ 11 20037
Paramètres sympas pour pytest http://sametmax.com/parametres-sympas-pour-pytest/ http://sametmax.com/parametres-sympas-pour-pytest/#comments Fri, 29 Jan 2016 12:19:35 +0000 http://sametmax.com/?p=17997 En Python, pas la peine de faire des tests sans pytest.

Et avec quelques options, pytest devient encore plus pratique:

py.test -vv --capture=no --showlocals --exitfirst

Détaillons.

-vv déclenche le mode extra-verbeux, qui affiche le nom de chaque test qui passe ainsi que plein de détails en cas d’échec.

--capture=no permet d’utiliser print() et pdb dans vos tests unitaires.

--showlocals affiche les variables locales de tout test qui échoue.

--exitfirst arrête les tests dès le premier échec plutôt que de faire toute la liste.

Grace au paramètre addopts, vous pouvez passer ces paramètres par défaut à py.test.

Soit par la variable d’environnement PYTEST_ADDOPTS, par exemple en mettant dans votre bashrc:

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

Ou en mettant dans le fichier de config (comme tox.ini)

[pytest]
addopts = -vv --capture=no --showlocals --exitfirst
]]>
http://sametmax.com/parametres-sympas-pour-pytest/feed/ 2 17997
Paramètres par défaut pour la commande py.test http://sametmax.com/parametres-par-defaut-pour-la-commande-py-test/ http://sametmax.com/parametres-par-defaut-pour-la-commande-py-test/#comments Fri, 03 May 2013 08:23:51 +0000 http://sametmax.com/?p=5969 pytest, et je me retrouve souvent à rentrer les mêmes paramètres de la commande encore et encore. Parfois, quand j'autilise des wrappers tels que django-pytest et pytest-django (ça s'invente pas), je ne peux même pas passer d'arguments directement à py.test. On peut y remedier en créer un fichier de config à la racine du projet.]]> Je ne fais plus de tests unitaires sans pytest, et je me retrouve souvent à rentrer les mêmes paramètres de la commande encore et encore. Parfois, quand j’utilise des wrappers tels que django-pytest et pytest-django (ça s’invente pas), je ne peux même pas passer d’arguments directement à py.test.

On peut y remédier en créant un fichier de config à la racine du projet. Nommez le fichier tox.ini, car c’est aussi le nom du fichier de configuration de l’outil de tests tox, et il est compatible, donc autant avoir un seul format. Dedans, créez une section [pytest], et vous pouvez configurer la lib là-dedans.

En l’occurrence, le settings “addopts”, pour “add options” (ajouter options), permet de spécifier les options de la ligne de commande à toujours ajouter à py.test.

Mon fichier contient toujours au moins ceci :

[pytest]
addopts = --ignore="virtualenv" --capture=no

Ainsi py.test ignore toujours le dossier virtualenv (qui est un lien vers l’env virtuel de mon projet) car je ne veux pas qu’il lance les tests de ce dossier. Et il ne capture pas stdout, ce qui me permet d’utiliser ipdb pendant les tests unitaires. Parfois j’utilise aussi --maxfail=1 quand je veux qu’il s’arrête dès la première erreur rencontrée.

Pour ne pas aller dans ce dossier et ne pas capturer stdout.

]]>
http://sametmax.com/parametres-par-defaut-pour-la-commande-py-test/feed/ 3 5969
Se simplifier les tests Python avec Pytest http://sametmax.com/se-simplifier-les-tests-python-avec-pytest/ http://sametmax.com/se-simplifier-les-tests-python-avec-pytest/#comments Wed, 07 Nov 2012 12:04:50 +0000 http://sametmax.com/?p=2884 assert, mais un résultat plus clair que unittest en sortie ?]]> 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']*>(.*)', 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.

]]>
http://sametmax.com/se-simplifier-les-tests-python-avec-pytest/feed/ 13 2884