Основные модели и примеры¶
Как изменить параметры командной строки по умолчанию¶
Может быть утомительно набирать одну и ту же серию опций командной строки каждый раз, когда вы используете pytest
. Например, если вы хотите всегда видеть подробную информацию о пропущенных и xfailed тестах, а также иметь более точечный вывод прогресса, вы можете записать это в конфигурационный файл:
# content of pytest.ini
[pytest]
addopts = -ra -q
В качестве альтернативы можно установить переменную среды PYTEST_ADDOPTS
, чтобы добавить параметры командной строки, пока используется среда:
export PYTEST_ADDOPTS="-v"
Вот как строится командная строка в присутствии addopts
или переменной окружения:
<pytest.ini:addopts> $PYTEST_ADDOPTS <extra command-line arguments>
Поэтому если пользователь выполняет в командной строке:
pytest -m slow
Фактическая командная строка выполняется следующим образом:
pytest -ra -q -v -m slow
Обратите внимание, что, как обычно для других приложений командной строки, в случае конфликтующих опций побеждает последняя, поэтому в приведенном выше примере будет показан многословный вывод, поскольку -v
переписал -q
.
Передавать различные значения в тестовую функцию в зависимости от опций командной строки¶
Предположим, мы хотим написать тест, который зависит от опции командной строки. Вот базовый шаблон для достижения этой цели:
# content of test_sample.py
def test_answer(cmdopt):
if cmdopt == "type1":
print("first")
elif cmdopt == "type2":
print("second")
assert 0 # to see what was printed
Чтобы это работало, нам нужно добавить опцию командной строки и предоставить cmdopt
через fixture function:
# content of conftest.py
import pytest
def pytest_addoption(parser):
parser.addoption(
"--cmdopt", action="store", default="type1", help="my option: type1 or type2"
)
@pytest.fixture
def cmdopt(request):
return request.config.getoption("--cmdopt")
Давайте запустим его без установки нашей новой опции:
$ pytest -q test_sample.py
F [100%]
================================= FAILURES =================================
_______________________________ test_answer ________________________________
cmdopt = 'type1'
def test_answer(cmdopt):
if cmdopt == "type1":
print("first")
elif cmdopt == "type2":
print("second")
> assert 0 # to see what was printed
E assert 0
test_sample.py:6: AssertionError
--------------------------- Captured stdout call ---------------------------
first
========================= short test summary info ==========================
FAILED test_sample.py::test_answer - assert 0
1 failed in 0.12s
А теперь с предоставлением опции командной строки:
$ pytest -q --cmdopt=type2
F [100%]
================================= FAILURES =================================
_______________________________ test_answer ________________________________
cmdopt = 'type2'
def test_answer(cmdopt):
if cmdopt == "type1":
print("first")
elif cmdopt == "type2":
print("second")
> assert 0 # to see what was printed
E assert 0
test_sample.py:6: AssertionError
--------------------------- Captured stdout call ---------------------------
second
========================= short test summary info ==========================
FAILED test_sample.py::test_answer - assert 0
1 failed in 0.12s
Вы можете видеть, что в нашем тесте появилась опция командной строки.
Мы можем добавить простую валидацию для ввода путем перечисления вариантов:
# content of conftest.py
import pytest
def pytest_addoption(parser):
parser.addoption(
"--cmdopt",
action="store",
default="type1",
help="my option: type1 or type2",
choices=("type1", "type2"),
)
Теперь мы получим отзыв на плохой аргумент:
$ pytest -q --cmdopt=type3
ERROR: usage: pytest [options] [file_or_dir] [file_or_dir] [...]
pytest: error: argument --cmdopt: invalid choice: 'type3' (choose from 'type1', 'type2')
Если вам необходимо предоставить более подробные сообщения об ошибках, вы можете использовать параметр type
и поднять значение pytest.UsageError
:
# content of conftest.py
import pytest
def type_checker(value):
msg = "cmdopt must specify a numeric type as typeNNN"
if not value.startswith("type"):
raise pytest.UsageError(msg)
try:
int(value[4:])
except ValueError:
raise pytest.UsageError(msg)
return value
def pytest_addoption(parser):
parser.addoption(
"--cmdopt",
action="store",
default="type1",
help="my option: type1 or type2",
type=type_checker,
)
На этом основной шаблон завершен. Однако часто возникает желание обрабатывать параметры командной строки вне теста и передавать в него различные или более сложные объекты.
Динамическое добавление параметров командной строки¶
С помощью addopts
вы можете статически добавить параметры командной строки для вашего проекта. Вы также можете динамически изменять аргументы командной строки до их обработки:
# setuptools plugin
import sys
def pytest_load_initial_conftests(args):
if "xdist" in sys.modules: # pytest-xdist plugin
import multiprocessing
num = max(multiprocessing.cpu_count() / 2, 1)
args[:] = ["-n", str(num)] + args
Если у вас установлен xdist plugin, то теперь вы всегда будете выполнять тестовые прогоны, используя ряд подпроцессов, близких к вашему CPU. Запуск в пустом каталоге с приведенным выше conftest.py:
$ pytest
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y
rootdir: /home/sweet/project
collected 0 items
========================== no tests ran in 0.12s ===========================
Управление пропуском тестов в соответствии с опцией командной строки¶
Вот conftest.py
файл, добавляющий опцию командной строки --runslow
для управления пропуском pytest.mark.slow
помеченных тестов:
# content of conftest.py
import pytest
def pytest_addoption(parser):
parser.addoption(
"--runslow", action="store_true", default=False, help="run slow tests"
)
def pytest_configure(config):
config.addinivalue_line("markers", "slow: mark test as slow to run")
def pytest_collection_modifyitems(config, items):
if config.getoption("--runslow"):
# --runslow given in cli: do not skip slow tests
return
skip_slow = pytest.mark.skip(reason="need --runslow option to run")
for item in items:
if "slow" in item.keywords:
item.add_marker(skip_slow)
Теперь мы можем написать тестовый модуль следующим образом:
# content of test_module.py
import pytest
def test_func_fast():
pass
@pytest.mark.slow
def test_func_slow():
pass
и при запуске он увидит пропущенный «медленный» тест:
$ pytest -rs # "-rs" means report details on the little 's'
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y
rootdir: /home/sweet/project
collected 2 items
test_module.py .s [100%]
========================= short test summary info ==========================
SKIPPED [1] test_module.py:8: need --runslow option to run
======================= 1 passed, 1 skipped in 0.12s =======================
Или запустите его, включая помеченный тест slow
:
$ pytest --runslow
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y
rootdir: /home/sweet/project
collected 2 items
test_module.py .. [100%]
============================ 2 passed in 0.12s =============================
Написание хорошо интегрированных помощников утверждений¶
Если у вас есть вспомогательная функция теста, вызываемая из теста, вы можете использовать маркер pytest.fail
для отказа теста с определенным сообщением. Вспомогательная функция теста не будет отображаться в трассировке, если вы установите опцию __tracebackhide__
где-нибудь в вспомогательной функции. Пример:
# content of test_checkconfig.py
import pytest
def checkconfig(x):
__tracebackhide__ = True
if not hasattr(x, "config"):
pytest.fail(f"not configured: {x}")
def test_something():
checkconfig(42)
Настройка __tracebackhide__
влияет на pytest
показ трассировок: функция checkconfig
не будет показана, если не указан параметр командной строки --full-trace
. Давайте запустим нашу маленькую функцию:
$ pytest -q test_checkconfig.py
F [100%]
================================= FAILURES =================================
______________________________ test_something ______________________________
def test_something():
> checkconfig(42)
E Failed: not configured: 42
test_checkconfig.py:11: Failed
========================= short test summary info ==========================
FAILED test_checkconfig.py::test_something - Failed: not configured: 42
1 failed in 0.12s
Если вы хотите скрыть только определенные исключения, вы можете установить __tracebackhide__
на вызываемый объект, который получает объект ExceptionInfo
. Это можно использовать, например, для того, чтобы не скрывать неожиданные типы исключений:
import operator
import pytest
class ConfigException(Exception):
pass
def checkconfig(x):
__tracebackhide__ = operator.methodcaller("errisinstance", ConfigException)
if not hasattr(x, "config"):
raise ConfigException(f"not configured: {x}")
def test_something():
checkconfig(42)
Это позволит избежать сокрытия трассировки исключения для несвязанных исключений (например, ошибок в помощниках утверждений).
Определите, запущен ли он в рамках выполнения pytest¶
Обычно это плохая идея - заставлять код приложения вести себя по-другому, если он вызывается из теста. Но если вам совершенно необходимо выяснить, запускается ли код приложения из теста, вы можете сделать что-то вроде этого:
# content of your_module.py
_called_from_test = False
# content of conftest.py
def pytest_configure(config):
your_module._called_from_test = True
а затем проверить наличие флага your_module._called_from_test
:
if your_module._called_from_test:
# called from within a test run
...
else:
# called "normally"
...
соответствующим образом в вашем заявлении.
Добавление информации в заголовок отчета о тестировании¶
Легко представить дополнительную информацию в прогоне pytest
:
# content of conftest.py
def pytest_report_header(config):
return "project deps: mylib-1.1"
который соответствующим образом добавит строку в заголовок теста:
$ pytest
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y
project deps: mylib-1.1
rootdir: /home/sweet/project
collected 0 items
========================== no tests ran in 0.12s ===========================
Также можно вернуть список строк, которые будут рассматриваться как несколько строк информации. Вы можете использовать config.getoption('verbose')
, чтобы вывести больше информации, если это необходимо:
# content of conftest.py
def pytest_report_header(config):
if config.getoption("verbose") > 0:
return ["info1: did you know that ...", "did you?"]
который будет добавлять информацию только при запуске с «–v»:
$ pytest -v
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y -- $PYTHON_PREFIX/bin/python
cachedir: .pytest_cache
info1: did you know that ...
did you?
rootdir: /home/sweet/project
collecting ... collected 0 items
========================== no tests ran in 0.12s ===========================
и ничего, если работать чисто:
$ pytest
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y
rootdir: /home/sweet/project
collected 0 items
========================== no tests ran in 0.12s ===========================
Профилирование продолжительности испытаний¶
Если у вас есть медленно работающий большой набор тестов, вы можете захотеть выяснить, какие тесты работают медленнее всего. Давайте создадим искусственный набор тестов:
# content of test_some_are_slow.py
import time
def test_funcfast():
time.sleep(0.1)
def test_funcslow1():
time.sleep(0.2)
def test_funcslow2():
time.sleep(0.3)
Теперь мы можем определить, какие тестовые функции выполняются медленнее всего:
$ pytest --durations=3
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y
rootdir: /home/sweet/project
collected 3 items
test_some_are_slow.py ... [100%]
=========================== slowest 3 durations ============================
0.30s call test_some_are_slow.py::test_funcslow2
0.20s call test_some_are_slow.py::test_funcslow1
0.10s call test_some_are_slow.py::test_funcfast
============================ 3 passed in 0.12s =============================
Инкрементальное тестирование - этапы тестирования¶
Иногда у вас может возникнуть ситуация тестирования, состоящая из серии тестовых шагов. Если один шаг не удался, нет смысла выполнять дальнейшие шаги, так как все они в любом случае ожидаются неудачными, и их отслеживание не добавляет никакой информации. Вот простой файл conftest.py
, который вводит маркер incremental
, который должен использоваться в классах:
# content of conftest.py
from typing import Dict, Tuple
import pytest
# store history of failures per test class name and per index in parametrize (if parametrize used)
_test_failed_incremental: Dict[str, Dict[Tuple[int, ...], str]] = {}
def pytest_runtest_makereport(item, call):
if "incremental" in item.keywords:
# incremental marker is used
if call.excinfo is not None:
# the test has failed
# retrieve the class name of the test
cls_name = str(item.cls)
# retrieve the index of the test (if parametrize is used in combination with incremental)
parametrize_index = (
tuple(item.callspec.indices.values())
if hasattr(item, "callspec")
else ()
)
# retrieve the name of the test function
test_name = item.originalname or item.name
# store in _test_failed_incremental the original name of the failed test
_test_failed_incremental.setdefault(cls_name, {}).setdefault(
parametrize_index, test_name
)
def pytest_runtest_setup(item):
if "incremental" in item.keywords:
# retrieve the class name of the test
cls_name = str(item.cls)
# check if a previous test has failed for this class
if cls_name in _test_failed_incremental:
# retrieve the index of the test (if parametrize is used in combination with incremental)
parametrize_index = (
tuple(item.callspec.indices.values())
if hasattr(item, "callspec")
else ()
)
# retrieve the name of the first test function to fail for this class name and index
test_name = _test_failed_incremental[cls_name].get(parametrize_index, None)
# if name found, test has failed for the combination of class name & test name
if test_name is not None:
pytest.xfail(f"previous test failed ({test_name})")
Эти две реализации хуков работают вместе для прерывания инкрементально помеченных тестов в классе. Вот пример тестового модуля:
# content of test_step.py
import pytest
@pytest.mark.incremental
class TestUserHandling:
def test_login(self):
pass
def test_modification(self):
assert 0
def test_deletion(self):
pass
def test_normal():
pass
Если мы запустим это:
$ pytest -rx
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y
rootdir: /home/sweet/project
collected 4 items
test_step.py .Fx. [100%]
================================= FAILURES =================================
____________________ TestUserHandling.test_modification ____________________
self = <test_step.TestUserHandling object at 0xdeadbeef0001>
def test_modification(self):
> assert 0
E assert 0
test_step.py:11: AssertionError
========================= short test summary info ==========================
XFAIL test_step.py::TestUserHandling::test_deletion - reason: previous test failed (test_modification)
================== 1 failed, 2 passed, 1 xfailed in 0.12s ==================
Мы увидим, что test_deletion
не был выполнен, потому что test_modification
потерпел неудачу. Об этом сообщается как об «ожидаемом сбое».
Приспособления (установки) на уровне пакетов/директорий¶
Если у вас есть вложенные каталоги тестов, вы можете иметь диапазоны фикстур для каждого каталога, поместив функции фикстур в файл conftest.py
в этом каталоге. Вы можете использовать все типы фикстур, включая autouse fixtures, которые являются эквивалентом концепции setup/teardown в xUnit. Однако рекомендуется иметь явные ссылки на фикстуры в ваших тестах или тестовых классах, а не полагаться на неявное выполнение функций setup/teardown, особенно если они находятся далеко от реальных тестов.
Вот пример того, как сделать приспособление db
доступным в каталоге:
# content of a/conftest.py
import pytest
class DB:
pass
@pytest.fixture(scope="session")
def db():
return DB()
и затем тестовый модуль в этой директории:
# content of a/test_db.py
def test_a1(db):
assert 0, db # to show value
другой тестовый модуль:
# content of a/test_db2.py
def test_a2(db):
assert 0, db # to show value
а затем модуль в соседнем каталоге, который не увидит фиксацию db
:
# content of b/test_error.py
def test_root(db): # no db here, will error out
pass
Мы можем запустить это:
$ pytest
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y
rootdir: /home/sweet/project
collected 7 items
test_step.py .Fx. [ 57%]
a/test_db.py F [ 71%]
a/test_db2.py F [ 85%]
b/test_error.py E [100%]
================================== ERRORS ==================================
_______________________ ERROR at setup of test_root ________________________
file /home/sweet/project/b/test_error.py, line 1
def test_root(db): # no db here, will error out
E fixture 'db' not found
> available fixtures: cache, capfd, capfdbinary, caplog, capsys, capsysbinary, doctest_namespace, monkeypatch, pytestconfig, record_property, record_testsuite_property, record_xml_attribute, recwarn, tmp_path, tmp_path_factory, tmpdir, tmpdir_factory
> use 'pytest --fixtures [testpath]' for help on them.
/home/sweet/project/b/test_error.py:1
================================= FAILURES =================================
____________________ TestUserHandling.test_modification ____________________
self = <test_step.TestUserHandling object at 0xdeadbeef0002>
def test_modification(self):
> assert 0
E assert 0
test_step.py:11: AssertionError
_________________________________ test_a1 __________________________________
db = <conftest.DB object at 0xdeadbeef0003>
def test_a1(db):
> assert 0, db # to show value
E AssertionError: <conftest.DB object at 0xdeadbeef0003>
E assert 0
a/test_db.py:2: AssertionError
_________________________________ test_a2 __________________________________
db = <conftest.DB object at 0xdeadbeef0003>
def test_a2(db):
> assert 0, db # to show value
E AssertionError: <conftest.DB object at 0xdeadbeef0003>
E assert 0
a/test_db2.py:2: AssertionError
========================= short test summary info ==========================
FAILED test_step.py::TestUserHandling::test_modification - assert 0
FAILED a/test_db.py::test_a1 - AssertionError: <conftest.DB object at 0x7...
FAILED a/test_db2.py::test_a2 - AssertionError: <conftest.DB object at 0x...
ERROR b/test_error.py::test_root
============= 3 failed, 2 passed, 1 xfailed, 1 error in 0.12s ==============
Два тестовых модуля в каталоге a
видят один и тот же экземпляр приспособления db
, в то время как один тест в сестринском каталоге b
его не видит. Конечно, мы могли бы также определить приспособление db
в файле conftest.py
сестринской директории. Обратите внимание, что каждое приспособление инстанцируется, только если тест действительно нуждается в нем (если только вы не используете приспособление «autouse», которое всегда выполняется перед выполнением первого теста).
Отчеты об испытаниях после обработки / сбои¶
Если вы хотите постпроцессировать отчеты о тестировании и вам нужен доступ к исполняющей среде, вы можете реализовать хук, который будет вызываться, когда объект «отчет» теста будет создан. Здесь мы записываем все неудачные вызовы теста, а также получаем доступ к фикстуре (если она использовалась тестом) на случай, если вы захотите запросить/просмотреть ее во время пост-обработки. В нашем случае мы просто записываем некоторую информацию в файл failures
:
# content of conftest.py
import os.path
import pytest
@pytest.hookimpl(tryfirst=True, hookwrapper=True)
def pytest_runtest_makereport(item, call):
# execute all other hooks to obtain the report object
outcome = yield
rep = outcome.get_result()
# we only look at actual failing test calls, not setup/teardown
if rep.when == "call" and rep.failed:
mode = "a" if os.path.exists("failures") else "w"
with open("failures", mode) as f:
# let's also access a fixture for the fun of it
if "tmp_path" in item.fixturenames:
extra = " ({})".format(item.funcargs["tmp_path"])
else:
extra = ""
f.write(rep.nodeid + extra + "\n")
если потом у вас будут неудачные тесты:
# content of test_module.py
def test_fail1(tmp_path):
assert 0
def test_fail2():
assert 0
и запустить их:
$ pytest test_module.py
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y
rootdir: /home/sweet/project
collected 2 items
test_module.py FF [100%]
================================= FAILURES =================================
________________________________ test_fail1 ________________________________
tmp_path = PosixPath('PYTEST_TMPDIR/test_fail10')
def test_fail1(tmp_path):
> assert 0
E assert 0
test_module.py:2: AssertionError
________________________________ test_fail2 ________________________________
def test_fail2():
> assert 0
E assert 0
test_module.py:6: AssertionError
========================= short test summary info ==========================
FAILED test_module.py::test_fail1 - assert 0
FAILED test_module.py::test_fail2 - assert 0
============================ 2 failed in 0.12s =============================
у вас будет файл «failures», который содержит идентификаторы неудачных тестов:
$ cat failures
test_module.py::test_fail1 (PYTEST_TMPDIR/test_fail10)
test_module.py::test_fail2
Обеспечение доступности информации о результатах тестирования в приспособлениях¶
Если вы хотите сделать отчеты о результатах тестирования доступными в финализаторах приспособлений, вот небольшой пример, реализованный с помощью локального плагина:
# content of conftest.py
import pytest
@pytest.hookimpl(tryfirst=True, hookwrapper=True)
def pytest_runtest_makereport(item, call):
# execute all other hooks to obtain the report object
outcome = yield
rep = outcome.get_result()
# set a report attribute for each phase of a call, which can
# be "setup", "call", "teardown"
setattr(item, "rep_" + rep.when, rep)
@pytest.fixture
def something(request):
yield
# request.node is an "item" because we use the default
# "function" scope
if request.node.rep_setup.failed:
print("setting up a test failed!", request.node.nodeid)
elif request.node.rep_setup.passed:
if request.node.rep_call.failed:
print("executing test failed", request.node.nodeid)
если потом у вас будут неудачные тесты:
# content of test_module.py
import pytest
@pytest.fixture
def other():
assert 0
def test_setup_fails(something, other):
pass
def test_call_fails(something):
assert 0
def test_fail2():
assert 0
и запустить его:
$ pytest -s test_module.py
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y
rootdir: /home/sweet/project
collected 3 items
test_module.py Esetting up a test failed! test_module.py::test_setup_fails
Fexecuting test failed test_module.py::test_call_fails
F
================================== ERRORS ==================================
____________________ ERROR at setup of test_setup_fails ____________________
@pytest.fixture
def other():
> assert 0
E assert 0
test_module.py:7: AssertionError
================================= FAILURES =================================
_____________________________ test_call_fails ______________________________
something = None
def test_call_fails(something):
> assert 0
E assert 0
test_module.py:15: AssertionError
________________________________ test_fail2 ________________________________
def test_fail2():
> assert 0
E assert 0
test_module.py:19: AssertionError
========================= short test summary info ==========================
FAILED test_module.py::test_call_fails - assert 0
FAILED test_module.py::test_fail2 - assert 0
ERROR test_module.py::test_setup_fails - assert 0
======================== 2 failed, 1 error in 0.12s ========================
Вы увидите, что финализаторы приспособлений могут использовать точную отчетную информацию.
PYTEST_CURRENT_TEST
переменная окружения¶
Иногда сеанс тестирования может застрять, и нет простого способа выяснить, какой тест застрял, например, если pytest был запущен в тихом режиме (-q
) или у вас нет доступа к выводу консоли. Это особенно проблематично, если проблема возникает лишь спорадически, знаменитый «flaky» вид тестов.
pytest
устанавливает переменную окружения PYTEST_CURRENT_TEST
при запуске тестов, которая может быть проверена утилитами мониторинга процессов или библиотеками типа psutil, чтобы определить, какой тест застрял, если это необходимо:
import psutil
for pid in psutil.pids():
environ = psutil.Process(pid).environ()
if "PYTEST_CURRENT_TEST" in environ:
print(f'pytest process {pid} running: {environ["PYTEST_CURRENT_TEST"]}')
Во время сеанса тестирования pytest установит PYTEST_CURRENT_TEST
на текущий тест nodeid и текущий этап, который может быть setup
, call
или teardown
.
Например, при выполнении одной тестовой функции с именем test_foo
из foo_module.py
, PYTEST_CURRENT_TEST
будет установлен:
foo_module.py::test_foo (setup)
foo_module.py::test_foo (call)
foo_module.py::test_foo (teardown)
В таком порядке.
Примечание
Содержимое PYTEST_CURRENT_TEST
предназначено для чтения человеком, и фактический формат может быть изменен между выпусками (даже исправлениями ошибок), поэтому его не следует использовать для сценариев или автоматизации.
Замораживание pytest¶
Если вы замораживаете свое приложение с помощью такого инструмента, как PyInstaller, чтобы распространить его среди конечных пользователей, хорошей идеей будет также упаковать программу запуска тестов и запустить тесты с использованием замороженного приложения. Таким образом, ошибки упаковки, такие как отсутствие зависимостей в исполняемом файле, могут быть обнаружены на ранней стадии, а также вы сможете отправить тестовые файлы пользователям, чтобы они могли запустить их на своих машинах, что может быть полезно для получения дополнительной информации о трудно воспроизводимой ошибке.
К счастью, в последних выпусках PyInstaller
уже есть собственный хук для pytest, но если вы используете другой инструмент для заморозки исполняемых файлов, например cx_freeze
или py2exe
, вы можете использовать pytest.freeze_includes()
для получения полного списка внутренних модулей pytest. Однако способ настройки инструментов для поиска внутренних модулей варьируется от инструмента к инструменту.
Вместо того чтобы замораживать программу pytest runner как отдельный исполняемый файл, вы можете заставить вашу замороженную программу работать как pytest runner с помощью умной обработки аргументов при запуске программы. Это позволит вам иметь один исполняемый файл, что обычно удобнее. Обратите внимание, что механизм обнаружения плагинов, используемый pytest (точки входа setuptools), не работает с замороженными исполняемыми файлами, поэтому pytest не сможет автоматически найти сторонние плагины. Для включения сторонних плагинов типа pytest-timeout
они должны быть импортированы явно и переданы в pytest.main.
# contents of app_main.py
import sys
import pytest_timeout # Third party plugin
if len(sys.argv) > 1 and sys.argv[1] == "--pytest":
import pytest
sys.exit(pytest.main(sys.argv[2:], plugins=[pytest_timeout]))
else:
# normal application execution: at this point argv can be parsed
# by your argument-parsing library of choice as usual
...
Это позволяет выполнять тесты с помощью замороженного приложения со стандартными опциями командной строки pytest
:
./app_main --pytest --verbose --tb=long --junitxml=results.xml test-suite/