Retour Suivant 
Les appareils intégrés fournis avec pytest peuvent vous aider à faire des choses assez utiles dans vos tests facilement et naturellement. Par exemple, en plus de gérer les fichiers temporaires, pytest comprend des appareils intégrés pour accéder aux paramètres de ligne de commande, la communication entre les sessions de test, la vérification des flux de sortie, la modification des variables d'environnement et les alertes d'interrogation.

Le code source du projet Tâches, ainsi que pour tous les tests présentés dans ce livre, est disponible sur le lien sur la page Web du livre à pragprog.com . Vous n'avez pas besoin de télécharger le code source pour comprendre le code de test; le code de test est présenté sous une forme pratique dans les exemples. Mais afin de suivre les tâches du projet ou d'adapter des exemples de test pour vérifier votre propre projet (vos mains sont déliées!), Vous devez vous rendre sur la page Web du livre pour télécharger le travail. Là, sur la page Web du livre, il y a un lien vers le message d' errata et le forum de discussion .
Sous le spoiler se trouve une liste d'articles de cette série.
Dans le chapitre précédent, vous avez examiné ce que sont les appareils, comment les écrire et comment les utiliser pour les données de test, ainsi que pour le code de configuration et de démontage.
Vous avez également utilisé conftest.py pour partager des appareils entre les tests dans plusieurs fichiers de test. À la fin du chapitre 3, les appareils suivants ont été installés dans les appareils pytest à la page 49 du projet Tâches: tasks_db_session
, tasks_just_a_few
, tasks_mult_per_owner
, tasks_db
, db_with_3_tasks
et db_with_multi_per_owner
défini dans conftest.py qui peut être utilisé par n'importe quelle fonction qui peut être utilisée par test en a besoin.
Réutiliser des appareils réguliers est une si bonne idée que les développeurs de pytest ont inclus certains appareils couramment requis dans pytest. Vous avez déjà vu comment tmpdir et tmpdir_factory sont utilisés par le projet Tâches dans la section de modification de l'étendue pour les appareils du projet Tâches à la page 59. Vous en discuterez plus en détail dans ce chapitre.
Les appareils intégrés fournis avec pytest peuvent vous aider à faire des choses assez utiles dans vos tests facilement et naturellement. Par exemple, en plus de gérer les fichiers temporaires, pytest comprend des appareils intégrés pour accéder aux paramètres de ligne de commande, la communication entre les sessions de test, la vérification des flux de sortie, la modification des variables d'environnement et les alertes d'interrogation. Les luminaires intégrés sont des extensions de la fonctionnalité de base de Pytest. Voyons maintenant quelques-uns des appareils en ligne les plus couramment utilisés dans l'ordre.
Utilisation de tmpdir et tmpdir_factory
Si vous testez quelque chose qui lit, écrit ou modifie des fichiers, vous pouvez utiliser tmpdir pour créer des fichiers ou des répertoires utilisés par un seul test, et vous pouvez utiliser tmpdir_factory
lorsque vous souhaitez configurer un répertoire pour plusieurs tests.
Le tmpdir
tmpdir a une portée de fonction, et le fixture tmpdir_factory
a une portée de session. Tout test unique qui nécessite un répertoire ou un fichier temporaire pour un seul test peut utiliser tmpdir
. Cela est également vrai pour les appareils qui personnalisent le répertoire ou le fichier qui doit être recréé pour chaque fonction de test.
Voici un exemple simple utilisant tmpdir
:
ch4/test_tmpdir.py
def test_tmpdir(tmpdir):
La valeur renvoyée par tmpdir
est un objet de type py.path.local.1
qui semble être tout ce dont nous avons besoin pour les répertoires et fichiers temporaires. Cependant, il y a une astuce. Étant donné que le dispositif tmpdir
défini comme une étendue de fonction , tmpdir
ne peut pas être utilisé pour créer des dossiers ou des fichiers qui doivent être disponibles pendant plus d'une fonction de test. Pour les appareils avec une portée autre qu'une fonction (classe, module, session), tmpdir_factory
est disponible.
Le tmpdir_factory
tmpdir_factory tmpdir_factory
très similaire à tmpdir
, mais a une interface différente. Comme décrit dans la section «Spécification des fixations de l'oscilloscope», page 56, les luminaires de zone de fonction s'exécutent une fois pour chaque fonction de test, les luminaires de région de module s'exécutent une fois par module, les luminaires de classe une fois pour chaque classe et les tests de validation de zone travailler une fois par session. Ainsi, les ressources créées dans les enregistrements de zone de session ont une durée de vie de la session entière. Pour montrer à quel point tmpdir
et tmpdir_factory
, je vais tmpdir
exemple tmpdir
, où tmpdir_factory
:
ch4 / test_tmpdir.py
def test_tmpdir_factory(tmpdir_factory):
La première ligne utilise mktemp('mydir')
pour créer le répertoire et le stocke dans a_dir
. Pour le reste de la fonction, vous pouvez utiliser a_dir
de la même manière que tmpdir
renvoyé par le tmpdir
.
Dans la deuxième ligne de l'exemple tmpdir_factory
, la fonction getbasetemp()
renvoie le répertoire de base utilisé pour cette session. L'instruction print dans l'exemple est nécessaire pour que vous puissiez afficher le répertoire sur votre système. Voyons où c'est:
$ cd /path/to/code/ch4 $ pytest -q -s test_tmpdir.py::test_tmpdir_factory base: /private/var/folders/53/zv4j_zc506x2xq25l31qxvxm0000gn/T/pytest-of-okken/pytest-732 . 1 passed in 0.04 seconds
Ce répertoire de base dépend du système et de l'utilisateur, et pytest - NUM
change pour chaque session avec un nombre croissant. Le répertoire de base est laissé seul après la session. pytest le nettoie et seuls les quelques répertoires de base temporaires les plus récents restent sur le système, ce qui est bien si vous êtes impatient de vérifier les fichiers après un test.
Vous pouvez également spécifier votre propre répertoire de base si vous en avez besoin avec pytest --basetemp=mydir
.
Utilisation de répertoires temporaires pour d'autres domaines
Nous obtenons des répertoires temporaires et des fichiers de zone de session du tmpdir_factory
, et des répertoires de fonctions et des fichiers de région du tmpdir
. Mais qu'en est-il des autres domaines? Et si nous avons besoin d'un répertoire temporaire de la portée d'un module ou d'une classe? Pour ce faire, nous créons un autre appareil de la région de la taille souhaitée et pour cela, nous devons utiliser tmpdir_factory
.
Par exemple, supposons que nous ayons un module rempli de tests, et que beaucoup d'entre eux devraient pouvoir lire certaines données d'un fichier json . Nous avons pu placer le volume du luminaire dans le module lui-même, ou dans le fichier conftest.py , qui configure le fichier de données comme suit:
ch4 / auteurs / conftest.py
"""Demonstrate tmpdir_factory.""" import json import pytest @pytest.fixture(scope='module') def author_file_json(tmpdir_factory): """ .""" python_author_data = { 'Ned': {'City': 'Boston'}, 'Brian': {'City': 'Portland'}, 'Luciano': {'City': 'Sau Paulo'} } file = tmpdir_factory.mktemp('data').join('author_file.json') print('file:{}'.format(str(file))) with file.open('w') as f: json.dump(python_author_data, f) return file
Le author_file_json()
crée un répertoire temporaire appelé data et crée un fichier appelé author_file.json dans le répertoire data. Ensuite, il écrit le dictionnaire python_author_data
en json . Puisqu'il s'agit d'un module de fixture area, un fichier json sera créé une seule fois pour chaque module à l'aide du test:
ch4 / auteurs / test_authors.py
""" , .""" import json def test_brian_in_portland(author_file_json): """, .""" with author_file_json.open() as f: authors = json.load(f) assert authors['Brian']['City'] == 'Portland' def test_all_have_cities(author_file_json): """ .""" with author_file_json.open() as f: authors = json.load(f) for a in authors: assert len(authors[a]['City']) > 0
Les deux tests utiliseront le même fichier JSON. Si un fichier de données de test fonctionne pour plusieurs tests, il est inutile de le recréer pour les deux tests.
Utilisation de pytestconfig
À l'aide du dispositif pytestconfig intégré, vous pouvez contrôler le fonctionnement de pytest avec les arguments et les options de ligne de commande, les fichiers de configuration, les plugins et le répertoire à partir duquel vous avez lancé pytest. Le fixture pytestconfig est un raccourci vers request.config, et est parfois appelé dans la documentation pytest « l'objet de configuration pytest » (objet de configuration pytest).
Pour savoir comment fonctionne pytestconfig , vous pouvez voir comment ajouter un paramètre de ligne de commande personnalisé et lire la valeur du paramètre à partir du test. Vous pouvez lire la valeur des paramètres de ligne de commande directement depuis pytestconfig, mais pour ajouter un paramètre et l'analyser, vous devez ajouter une fonction hook. Les fonctions de hook , que je décris plus en détail dans le Chapitre 5, «Plugins», page 95, sont une autre façon de contrôler le comportement de pytest et sont souvent utilisées dans les plugins. Cependant, l'ajout d'une option de ligne de commande personnalisée et sa lecture à partir de pytestconfig est assez répandu, donc je veux couvrir cela ici.
Nous allons utiliser le crochet pytest_addoption
pour ajouter plusieurs paramètres aux paramètres déjà disponibles sur la ligne de commande pytest:
ch4 / pytestconfig / conftest.py
def pytest_addoption(parser): parser.addoption("--myopt", action="store_true", help="some boolean option") parser.addoption("--foo", action="store", default="bar", help="foo: bar or baz")
L'ajout de paramètres de ligne de commande via pytest_addoption
doit être effectué via des plugins ou dans le fichier conftest.py situé en haut de la structure du répertoire du projet. Vous ne devriez pas faire cela dans le sous-répertoire test.
Les --myopt
et --foo <value>
ont été ajoutées au code précédent et la ligne d'aide a été modifiée comme indiqué ci-dessous:
$ cd /path/to/code/ch4/pytestconfig $ pytest --help usage: pytest [options] [file_or_dir] [file_or_dir] [...] ... custom options: --myopt some boolean option --foo=FOO foo: bar or baz ...
Maintenant, nous pouvons accéder à ces options à partir du test:
ch4 / pytestconfig / test_config.py
import pytest def test_option(pytestconfig): print('"foo" set to:', pytestconfig.getoption('foo')) print('"myopt" set to:', pytestconfig.getoption('myopt'))
Voyons comment cela fonctionne:
$ pytest -s -q test_config.py::test_option "foo" set to: bar "myopt" set to: False .1 passed in 0.01 seconds $ pytest -s -q --myopt test_config.py::test_option "foo" set to: bar "myopt" set to: True .1 passed in 0.01 seconds $ pytest -s -q --myopt --foo baz test_config.py::test_option "foo" set to: baz "myopt" set to: True .1 passed in 0.01 seconds
Puisque pytestconfig est un luminaire, il peut également être obtenu à partir d'autres luminaires. Vous pouvez créer des fixations pour les noms d'options si vous le souhaitez, par exemple:
ch4 / pytestconfig / test_config.py
@pytest.fixture() def foo(pytestconfig): return pytestconfig.option.foo @pytest.fixture() def myopt(pytestconfig): return pytestconfig.option.myopt def test_fixtures_for_options(foo, myopt): print('"foo" set to:', foo) print('"myopt" set to:', myopt)
Vous pouvez également accéder aux paramètres intégrés, pas seulement ceux ajoutés, ainsi qu'aux informations sur le lancement de pytest (répertoire, arguments, etc.).
Voici un exemple de plusieurs valeurs et options de configuration:
def test_pytestconfig(pytestconfig): print('args :', pytestconfig.args) print('inifile :', pytestconfig.inifile) print('invocation_dir :', pytestconfig.invocation_dir) print('rootdir :', pytestconfig.rootdir) print('-k EXPRESSION :', pytestconfig.getoption('keyword')) print('-v, --verbose :', pytestconfig.getoption('verbose')) print('-q, --quiet :', pytestconfig.getoption('quiet')) print('-l, --showlocals:', pytestconfig.getoption('showlocals')) print('--tb=style :', pytestconfig.getoption('tbstyle'))
Nous reviendrons sur pytestconfig lorsque je présenterai les fichiers ini dans le chapitre 6, «Configuration», page 113.
Utilisation du cache
Habituellement, nous, les testeurs, pensons que chaque test est aussi indépendant que possible des autres tests. Vous devez vous assurer que les dépendances de la comptabilité des commandes ne se sont pas glissées. J'aimerais pouvoir exécuter ou redémarrer n'importe quel test dans n'importe quel ordre et obtenir le même résultat. De plus, les sessions de test doivent être répétables et ne pas changer de comportement en fonction des sessions de test précédentes.
Cependant, le transfert d'informations d'une session de test à une autre peut parfois être très utile. Lorsque nous voulons transmettre des informations à de futures sessions de test, nous pouvons le faire avec le dispositif de cache
intégré.
Le cache
appareils cache
conçu pour stocker des informations sur une session de test et les obtenir dans la suivante. Un bon exemple d'utilisation des autorisations de cache
au profit de l'affaire est la fonctionnalité intégrée --last-failed
et --failed-first
. Voyons comment les données de ces indicateurs sont stockées dans le cache.
Voici le texte d'aide pour les options --last-failed
et --failed-first
, ainsi que certaines options de cache
:
$ pytest --help ... --lf, --last-failed rerun only the tests that failed at the last run (or all if none failed) --ff, --failed-first run all tests but run the last failures first. This may re-order tests and thus lead to repeated fixture setup/teardown --cache-show show cache contents, don t perform collection or tests --cache-clear remove all cache contents at start of test run. ...
Pour les voir en action, nous allons utiliser ces deux tests:
ch4 / cache / test_pass_fail.py
def test_this_passes(): assert 1 == 1 def test_this_fails(): assert 1 == 2
--verbose
les en utilisant --verbose
pour voir les noms des fonctions et --tb=no
pour masquer la trace de la pile:
$ cd /path/to/code/ch4/cache $ pytest --verbose --tb=no test_pass_fail.py ==================== test session starts ==================== collected 2 items test_pass_fail.py::test_this_passes PASSED test_pass_fail.py::test_this_fails FAILED ============ 1 failed, 1 passed in 0.05 seconds =============
Si vous les réexécutez avec l' --ff
ou --failed-first
, les tests qui ont échoué précédemment seront exécutés en premier, puis toute la session:
$ pytest --verbose --tb=no --ff test_pass_fail.py ==================== test session starts ==================== run-last-failure: rerun last 1 failures first collected 2 items test_pass_fail.py::test_this_fails FAILED test_pass_fail.py::test_this_passes PASSED ============ 1 failed, 1 passed in 0.04 seconds =============
Ou vous pouvez utiliser --lf
ou --last-failed
pour simplement exécuter les tests qui ont échoué la dernière fois:
$ pytest --verbose --tb=no --lf test_pass_fail.py ==================== test session starts ==================== run-last-failure: rerun last 1 failures collected 2 items test_pass_fail.py::test_this_fails FAILED ==================== 1 tests deselected ===================== ========== 1 failed, 1 deselected in 0.05 seconds ===========
Avant de voir comment les données de plantage sont stockées et comment vous pouvez utiliser le même mécanisme, regardons un autre exemple qui rend la valeur de --lf
et --ff
encore plus évidente.
Voici un test paramétré avec un échec:
ch4 / cache / test_few_failures.py
"""Demonstrate -lf and -ff with failing tests.""" import pytest from pytest import approx testdata = [
Et en sortie:
$ cd /path/to/code/ch4/cache $ pytest -q test_few_failures.py .F... ====================== FAILURES ====================== _________________________ test_a[1e+25-1e+23-1.1e+25] _________________________ x = 1e+25, y = 1e+23, expected = 1.1e+25 @pytest.mark.parametrize("x,y,expected", testdata) def test_a(x, y, expected): """Demo approx().""" sum_ = x + y > assert sum_ == approx(expected) E assert 1.01e+25 == 1.1e+25 ± 1.1e+19 E + where 1.1e+25 ± 1.1e+19 = approx(1.1e+25) test_few_failures.py:17: AssertionError 1 failed, 4 passed in 0.06 seconds
Vous pouvez peut-être identifier le problème tout de suite. Mais imaginons que le test est plus long et plus compliqué, et ce qui ne va pas ici n'est pas si évident. Exécutons à nouveau le test pour voir à nouveau l'erreur. Le scénario de test peut être spécifié sur la ligne de commande:
$ pytest -q "test_few_failures.py::test_a[1e+25-1e+23-1.1e+25]"
Si vous ne voulez pas copier-coller (copier / coller ) ou si vous souhaitez redémarrer plusieurs cas malheureux, alors --lf
beaucoup plus facile. Et si vous --showlocals
vraiment un échec de test, un autre indicateur qui peut faciliter la situation est --showlocals
, ou -l
pour faire court:
$ pytest -q --lf -l test_few_failures.py F ====================== FAILURES ====================== _________________________ test_a[1e+25-1e+23-1.1e+25] _________________________ x = 1e+25, y = 1e+23, expected = 1.1e+25 @pytest.mark.parametrize("x,y,expected", testdata) def test_a(x, y, expected): """Demo approx().""" sum_ = x + y > assert sum_ == approx(expected) E assert 1.01e+25 == 1.1e+25 ± 1.1e+19 E + where 1.1e+25 ± 1.1e+19 = approx(1.1e+25) expected = 1.1e+25 sum_ = 1.01e+25 x = 1e+25 y = 1e+23 test_few_failures.py:17: AssertionError ================= 4 tests deselected ================= 1 failed, 4 deselected in 0.05 seconds
La raison de l'échec devrait être plus évidente.
Afin de garder à l'esprit que le test a échoué la dernière fois, il y a une petite astuce. pytest stocke les informations sur les erreurs de test de la dernière session de test et vous pouvez afficher les informations enregistrées avec --cache-show
:
$ pytest --cache-show ===================== test session starts ====================== ------------------------- cache values ------------------------- cache/lastfailed contains: {'test_few_failures.py::test_a[1e+25-1e+23-1.1e+25]': True} ================= no tests ran in 0.00 seconds =================
Ou vous pouvez regarder dans le répertoire cache:
$ cat .cache/v/cache/lastfailed { "test_few_failures.py::test_a[1e+25-1e+23-1.1e+25]": true }
Le --clear-cache
vous permet de vider le cache avant une session.
Le cache peut être utilisé non seulement pour --lf
et --ff
. Écrivons un appareil qui enregistre le temps que prennent les tests, fait gagner du temps et la prochaine fois qu'il signale une erreur dans les tests qui prennent deux fois plus de temps que, disons, la dernière fois.
L'interface du dispositif de cache est simple.
cache.get(key, default) cache.set(key, value)
Par convention, les noms de clé commencent par le nom de votre application ou plug-in, suivi de /
et continuent de séparer les sections de nom de clé par /
. La valeur que vous stockez peut être toute valeur convertie en json , car elle est représentée dans le .cache directory
.
Voici notre appareil utilisé pour fixer le temps de test:
ch4 / cache / test_slower.py
@pytest.fixture(autouse=True) def check_duration(request, cache): key = 'duration/' + request.node.nodeid.replace(':', '_') # (nodeid) # .cache # - start_time = datetime.datetime.now() yield stop_time = datetime.datetime.now() this_duration = (stop_time - start_time).total_seconds() last_duration = cache.get(key, None) cache.set(key, this_duration) if last_duration is not None: errorstring = " 2- " assert this_duration <= last_duration * 2, errorstring
Étant donné que le luminaire est autouse , il n'a pas besoin d'être référencé à partir du test. L'objet request est utilisé pour obtenir le nodeid à utiliser dans la clé. nodeid est un identifiant unique qui fonctionne même avec des tests paramétrés. Nous ajoutons une clé avec 'duration /' pour être des résidents respectables du cache. Le code ci-dessus est exécuté avant la fonction de test; le code après rendement est exécuté après la fonction de test.
Maintenant, nous avons besoin de tests qui prennent des intervalles de temps différents:
ch4 / cache / test_slower.py
@pytest.mark.parametrize('i', range(5)) def test_slow_stuff(i): time.sleep(random.random())
Étant donné que vous ne voulez probablement pas écrire un tas de tests pour cela, j'ai utilisé random
et le paramétrage pour générer facilement des tests qui dorment pendant une durée aléatoire, tous plus courts qu'une seconde. Voyons quelques fois comment cela fonctionne:
$ cd /path/to/code/ch4/cache $ pytest -q --cache-clear test_slower.py ..... 5 passed in 2.10 seconds $ pytest -q --tb=line test_slower.py ...E..E =================================== ERRORS ==================================== ___________________ ERROR at teardown of test_slow_stuff[1] ___________________ E AssertionError: test duration over 2x last duration assert 0.35702 <= (0.148009 * 2) ___________________ ERROR at teardown of test_slow_stuff[4] ___________________ E AssertionError: test duration over 2x last duration assert 0.888051 <= (0.324019 * 2) 5 passed, 2 error in 3.17 seconds
Eh bien, c'était amusant. Voyons ce qu'il y a dans le cache:
$ pytest -q --cache-show -------------------------------- cache values --------------------------------- cache\lastfailed contains: {'test_slower.py::test_slow_stuff[2]': True, 'test_slower.py::test_slow_stuff[4]': True} cache\nodeids contains: ['test_slower.py::test_slow_stuff[0]', 'test_slower.py::test_slow_stuff[1]', 'test_slower.py::test_slow_stuff[2]', 'test_slower.py::test_slow_stuff[3]', 'test_slower.py::test_slow_stuff[4]'] cache\stepwise contains: [] duration\test_slower.py__test_slow_stuff[0] contains: 0.958055 duration\test_slower.py__test_slow_stuff[1] contains: 0.214012 duration\test_slower.py__test_slow_stuff[2] contains: 0.19001 duration\test_slower.py__test_slow_stuff[3] contains: 0.725041 duration\test_slower.py__test_slow_stuff[4] contains: 0.836048 no tests ran in 0.03 seconds
Vous pouvez facilement voir les données de duration
séparément des données de cache en raison du préfixe des noms de données de cache. Cependant, il est intéressant de lastfailed
que la fonctionnalité lastfailed
peut fonctionner avec une seule entrée de cache. Nos données de durée occupent une entrée de cache pour chaque test. Suivons l'exemple du dernier échec et mettons nos données dans un seul enregistrement.
Nous lisons et mettons en cache pour chaque test. Nous pouvons diviser le fixture en fixture de la portée de la fonction pour mesurer la durée et la fixture de la portée de la session pour la lecture et l'écriture dans le cache. Cependant, si nous le faisons, nous ne pourrons pas utiliser le dispositif de cache car il a la portée de la fonction. Heureusement, un rapide coup d'œil à l'implémentation sur GitHub montre que le dispositif de mise en cache renvoie simplement request.config.cache
. Il est disponible dans tous les domaines.
:
ch4/cache/test_slower_2.py
Duration = namedtuple('Duration', ['current', 'last']) @pytest.fixture(scope='session') def duration_cache(request): key = 'duration/testdurations' d = Duration({}, request.config.cache.get(key, {})) yield d request.config.cache.set(key, d.current) @pytest.fixture(autouse=True) def check_duration(request, duration_cache): d = duration_cache nodeid = request.node.nodeid start_time = datetime.datetime.now() yield duration = (datetime.datetime.now() - start_time).total_seconds() d.current[nodeid] = duration if d.last.get(nodeid, None) is not None: errorstring = "test duration over 2x last duration" assert duration <= (d.last[nodeid] * 2), errorstring
duration_cache
. , , - . , namedtuple
Duration current
last
. namedtuple
test_duration
, . , namedtuple
, d.current
. .
, :
$ pytest -q --cache-clear test_slower_2.py ..... 5 passed in 2.80 seconds $ pytest -q --tb=no test_slower_2.py ...EE.. 7 passed, 2 error in 3.21 seconds $ pytest -q --cache-show -------------------------------- cache values --------------------------------- cache\lastfailed contains: {'test_slower_2.py::test_slow_stuff[2]': True, 'test_slower_2.py::test_slow_stuff[3]': True} duration\testdurations contains: {'test_slower_2.py::test_slow_stuff[0]': 0.483028, 'test_slower_2.py::test_slow_stuff[1]': 0.198011, 'test_slower_2.py::test_slow_stuff[2]': 0.426024, 'test_slower_2.py::test_slow_stuff[3]': 0.762044, 'test_slower_2.py::test_slow_stuff[4]': 0.056003, 'test_slower_2.py::test_slow_stuff[5]': 0.18401, 'test_slower_2.py::test_slow_stuff[6]': 0.943054} no tests ran in 0.02 seconds
.
capsys
capsys builtin
: stdout stderr , . stdout stderr.
, stdout:
ch4/cap/test_capsys.py
def greeting(name): print('Hi, {}'.format(name))
, . - stdout. capsys:
ch4/cap/test_capsys.py
def test_greeting(capsys): greeting('Earthling') out, err = capsys.readouterr() assert out == 'Hi, Earthling\n' assert err == '' greeting('Brian') greeting('Nerd') out, err = capsys.readouterr() assert out == 'Hi, Brian\nHi, Nerd\n' assert err == ''
stdout
stderr
capsys.redouterr()
. — , , .
stdout
. , stderr
:
def yikes(problem): print('YIKES! {}'.format(problem), file=sys.stderr) def test_yikes(capsys): yikes('Out of coffee!') out, err = capsys.readouterr() assert out == '' assert 'Out of coffee!' in err
pytest . print . . -s
, stdout . , , . , pytest , , . capsys . capsys.disabled()
, .
Voici un exemple:
ch4/cap/test_capsys.py
def test_capsys_disabled(capsys): with capsys.disabled(): print('\nalways print this')
, 'always print this' :
$ cd /path/to/code/ch4/cap $ pytest -q test_capsys.py::test_capsys_disabled
, always print this
, capys.disabled()
. print — print, normal print, usually captured
( , ), , -s
, --capture=no
, .
monkeypatch
"monkey patch" — . "monkey patching" — , , . monkeypatch
. , , , , . , . API , monkeypatch .
monkeypatch
:
setattr(target, name, value=<notset>, raising=True)
: .delattr(target, name=<notset>, raising=True)
: .setitem(dic, name, value)
: .delitem(dic, name, raising=True)
: .setenv(name, value, prepend=None)
: .delenv(name, raising=True)
: .syspath_prepend(path)
: sys., Python.chdir(path)
: .
raising
pytest, , . prepend
setenv()
. , + prepend + <old value>
.
monkeypatch , , dot- . , dot- . , cheese- :
ch4/monkey/cheese.py
import os import json def read_cheese_preferences(): full_path = os.path.expanduser('~/.cheese.json') with open(full_path, 'r') as f: prefs = json.load(f) return prefs def write_cheese_preferences(prefs): full_path = os.path.expanduser('~/.cheese.json') with open(full_path, 'w') as f: json.dump(prefs, f, indent=4) def write_default_cheese_preferences(): write_cheese_preferences(_default_prefs) _default_prefs = { 'slicing': ['manchego', 'sharp cheddar'], 'spreadable': ['Saint Andre', 'camembert', 'bucheron', 'goat', 'humbolt fog', 'cambozola'], 'salads': ['crumbled feta'] }
, write_default_cheese_preferences()
. , . , . .
, . , read_cheese_preferences()
, , write_default_cheese_preferences()
:
ch4/monkey/test_cheese.py
def test_def_prefs_full(): cheese.write_default_cheese_preferences() expected = cheese._default_prefs actual = cheese.read_cheese_preferences() assert expected == actual
, , , cheese- . .
HOME set
, os.path.expanduser()
~
, HOME
. HOME
, :
ch4/monkey/test_cheese.py
def test_def_prefs_change_home(tmpdir, monkeypatch): monkeypatch.setenv('HOME', tmpdir.mkdir('home')) cheese.write_default_cheese_preferences() expected = cheese._default_prefs actual = cheese.read_cheese_preferences() assert expected == actual
, HOME
. - expanduser()
, , «On Windows, HOME and USERPROFILE will be used if set, otherwise a combination of….» . Ouah! , Windows. , .
, HOME
, expanduser
:
ch4/monkey/test_cheese.py
def test_def_prefs_change_expanduser(tmpdir, monkeypatch): fake_home_dir = tmpdir.mkdir('home') monkeypatch.setattr(cheese.os.path, 'expanduser', (lambda x: x.replace('~', str(fake_home_dir)))) cheese.write_default_cheese_preferences() expected = cheese._default_prefs actual = cheese.read_cheese_preferences() assert expected == actual
, cheese os.path.expanduser()
-. re.sub
~
. setenv()
setattr()
. , setitem()
.
, , , . , , write_default_cheese_preferences()
:
ch4/monkey/test_cheese.py
def test_def_prefs_change_defaults(tmpdir, monkeypatch):
_default_prefs
- , monkeypatch.setitem()
, .
setenv()
, setattr()
setitem()
. del
. , , - . monkeypatch
.
syspath_prepend(path)
sys.path
, , . stub-. monkeypatch.syspath_prepend()
, , stub-.
chdir(path)
. , . , , monkeypatch.chdir(the_tmpdir)
.
monkeypatch unittest.mock
, . 7 " pytest " . 125.
doctest_namespace
doctest Python docstrings , , . pytest doctest Python --doctest-modules
. doctest_namespace
, autouse , pytest doctest . docstrings .
doctest_namespace
, Python . , numpy
import numpy as np
.
. , unnecessary_math.py
multiply()
divide()
, . , docstring , docstrings :
ch4/dt/1/unnecessary_math.py
""" This module defines multiply(a, b) and divide(a, b). >>> import unnecessary_math as um Here's how you use multiply: >>> um.multiply(4, 3) 12 >>> um.multiply('a', 3) 'aaa' Here's how you use divide: >>> um.divide(10, 5) 2.0 """ def multiply(a, b): """ Returns a multiplied by b. >>> um.multiply(4, 3) 12 >>> um.multiply('a', 3) 'aaa' """ return a * b def divide(a, b): """ Returns a divided by b. >>> um.divide(10, 5) 2.0 """ return a / b
unnecessary_math
, um
, import noecessary_math as um
-. docstrings import
, um
. , pytest docstring . docstring , docstrings :
$ cd /path/to/code/ch4/dt/1 $ pytest -v --doctest-modules --tb=short unnecessary_math.py ============================= test session starts ============================= collected 3 items unnecessary_math.py::unnecessary_math PASSED unnecessary_math.py::unnecessary_math.divide FAILED unnecessary_math.py::unnecessary_math.multiply FAILED ================================== FAILURES =================================== ______________________ [doctest] unnecessary_math.divide ______________________ 034 035 Returns a divided by b. 036 037 >>> um.divide(10, 5) UNEXPECTED EXCEPTION: NameError("name 'um' is not defined",) Traceback (most recent call last): ... File "<doctest unnecessary_math.divide[0]>", line 1, in <module> NameError: name 'um' is not defined ... _____________________ [doctest] unnecessary_math.multiply _____________________ 022 023 Returns a multiplied by b. 024 025 >>> um.multiply(4, 3) UNEXPECTED EXCEPTION: NameError("name 'um' is not defined",) Traceback (most recent call last): ... File "<doctest unnecessary_math.multiply[0]>", line 1, in <module> NameError: name 'um' is not defined /path/to/code/ch4/dt/1/unnecessary_math.py:23: UnexpectedException ================ 2 failed, 1 passed in 0.03 seconds =================
- import
docstring:
ch4/dt/2/unnecessary_math.py
""" This module defines multiply(a, b) and divide(a, b). >>> import unnecessary_math as um Here's how you use multiply: >>> um.multiply(4, 3) 12 >>> um.multiply('a', 3) 'aaa' Here's how you use divide: >>> um.divide(10, 5) 2.0 """ def multiply(a, b): """ Returns a multiplied by b. >>> import unnecessary_math as um >>> um.multiply(4, 3) 12 >>> um.multiply('a', 3) 'aaa' """ return a * b def divide(a, b): """ Returns a divided by b. >>> import unnecessary_math as um >>> um.divide(10, 5) 2.0 """ return a / b
:
$ cd /path/to/code/ch4/dt/2 $ pytest -v --doctest-modules --tb=short unnecessary_math.py ============================= test session starts ============================= collected 3 items unnecessary_math.py::unnecessary_math PASSED [ 33%] unnecessary_math.py::unnecessary_math.divide PASSED [ 66%] unnecessary_math.py::unnecessary_math.multiply PASSED [100%] ===================== 3 passed in 0.03 seconds ======================
docstrings .
doctest_namespace
, autouse
conftest.py
, :
ch4/dt/3/conftest.py
import pytest import unnecessary_math @pytest.fixture(autouse=True) def add_um(doctest_namespace): doctest_namespace['um'] = unnecessary_math
pytest um
doctest_namespace
, unnecessary_math
. conftest.py, doctests, conftest.py um
.
doctest pytest 7 " pytest " . 125.
recwarn
recwarn
, . Python , , , . , , , , . :
ch4/test_warnings.py
import warnings import pytest def lame_function(): warnings.warn("Please stop using this", DeprecationWarning)
, :
ch4/test_warnings.py
def test_lame_function(recwarn): lame_function() assert len(recwarn) == 1 w = recwarn.pop() assert w.category == DeprecationWarning assert str(w.message) == 'Please stop using this'
recwarn , category
(), message
(), filename
( ) lineno
( ), .
. , , , . recwarn.clear()
, , .
recwarn
, pytest pytest.warns()
:
ch4/test_warnings.py
def test_lame_function_2(): with pytest.warns(None) as warning_list: lame_function() assert len(warning_list) == 1 w = warning_list.pop() assert w.category == DeprecationWarning assert str(w.message) == 'Please stop using this'
pytest.warns()
. recwarn
pytest.warns()
, , , .
Exercices
ch4/cache/test_slower.py
autouse
, check_duration()
. ch3/tasks_proj/tests/conftest.py
.- Effectuez les tests du chapitre 3.
- Pour les tests vraiment très rapides, 2x vraiment rapide est toujours très rapide. Au lieu de 2x, changez le luminaire pour vérifier 0,1 seconde plus 2x pour la dernière durée.
- Exécutez pytest avec le luminaire modifié. Les résultats semblent-ils raisonnables?
Et ensuite
Dans ce chapitre, vous avez examiné certains des appareils Pytest intégrés. Ensuite, vous considérerez les plugins plus en détail. Les nuances de l'écriture de gros plugins peuvent devenir un livre en soi; cependant, les petits plugins personnalisés font régulièrement partie de l'écosystème pytest.
Retour Suivant 