pytest
;pytest
и sys.path/PYTHONPATH
;Для разработки и интеграции с проектом pytest
рекомендуется использовать виртуальные среды venv
и pip для установки приложения и любых зависимостей, а также сам фреймворк pytest
. Это гарантирует, что код и зависимости будут изолированы от системной установки Python.
Затем расположим файл pyproject.toml
в корень проекта:
[build-system] requires = ["setuptools>=42", "wheel"] build-backend = "setuptools.build_meta"
и файл setup.cfg
, содержащий метаданные разрабатываемого проекта со следующим минимальным содержимым, где PACKAGENAME - это имя проекта:
[metadata] name = PACKAGENAME [options] packages = find:
Примечание. Если версия установленного pip
старше 21.3, то также понадобится файл setup.py
:
from setuptools import setup, find_packages setup(name="PACKAGENAME", packages=find_packages())
Затем можно установить свой проект в "режиме изменений", запустив из того же каталога pip install -e .
, который позволяет изменять исходный код (как тесты, так и приложение) и повторно запускать тесты по желанию.
pytest
.Фреймворк pytest
реализует следующее стандартное обнаружение тестов:
Если при запуске pytest
параметры CLI не указаны, то сбор начинается с тестовых путей testpaths
(если они настроены) или текущего каталога. В качестве альтернативы, можно использовать параметры командной строки с любой комбинацией директорий, имен файлов и идентификаторов узлов.
Примечание: testpaths
устанавливает список каталогов, в которых следует искать тесты, если в командной строке не указаны конкретные каталоги, файлы или идентификаторы тестов при выполнении тестов из каталога rootdir
. Полезно, когда все тесты проекта находятся в известном месте, чтобы ускорить сбор тестов и избежать случайного выбора нежелательных тестов.
# указывается в `setup.cfg` или `pytest.ini` [pytest] testpaths = testing doc
Данная запись говорит pytest
, что при выполнении из корневого каталога, искать тесты нужно только в каталогах testing
и doc
.
Проводится рекурсивный сбор тестов по всем каталогам проекта, если только они не совпадают с norecursedirs
.
Примечание: norecursedirs
устанавливает шаблоны базовых имен каталогов, которые следует избегать при рекурсивном обнаружении тестов. К базовому имени каталога будут применяться индивидуальные шаблоны в стиле fnmatch
. Шаблонами по умолчанию являются: '*.egg'
, '.*'
, '_darcs'
, 'build'
, 'CVS'
, 'dist'
, 'node_modules'
, 'venv'
, '{arch}'
. Установка значения norecursedirs
заменяет значение по умолчанию. Пример того, как избежать определенных каталогов:
# указывается в `setup.cfg` или `pytest.ini` [pytest] norecursedirs = .svn _build tmp*
Данная запись говорит pytest
, что не надо обходить типичные каталоги subversion
или sphinx-build
или в любой каталог с префиксом tmp
.
Кроме того, pytest
попытается разумно идентифицировать и игнорировать virtualenv
по наличию скрипта активации. Любой каталог, который считается корневым каталогом виртуальной среды, не будет рассматриваться во время сбора тестов, если не задан параметр --collect‑in‑virtualenv
. Обратите внимание, что norecursedirs имеет приоритет над --collect‑in‑virtualenv
. Например, если запускать тесты в virtualenv
с базовым каталогом, который соответствует '.*', то необходимо переопределить norecursedirs
в дополнение к использованию флага --collect‑in‑virtualenv
.
Происходит поиск файлов с именами test_*.py
или *_test.py
, импортированные по имени их тестового пакета.
Из найденных файлов собираются тестовые элементы:
test_
, расположенные вне классов;test_
, расположенные внутри тестовых классов (с префиксом Test_
и без метода __init__
).В модулях Python, pytest
также обнаруживает тесты, используя стандартный метод создания подклассов unittest.TestCase
.
Также смотрите "Как запускать/вызывать тесты pytest
в Python".
Фреимворк pytest
поддерживает два распространенных тестовых макета:
Помещение тестов в дополнительный каталог вне фактического кода приложения может быть полезно, если существует много функциональных тестов или по другим причинам желательно хранить тесты отдельно от фактического кода приложения (часто это хорошая идея):
pyproject.toml setup.cfg mypkg/ __init__.py app.py view.py tests/ test_app.py test_view.py ...
Это имеет следующие преимущества:
pip install
.pip install --editable
.sys.path
, можно выполнить python -m pytest
для выполнения тестов с локальной копией напрямую, без используя pip
.Ниже смотрите в чем разница между вызовом pytest
и python -m pytest
в подразделе "Режимы импорта".
Обратите внимание, что если используется режим предварительного импорта (который используется по умолчанию), то у этой схемы есть недостаток: тестовые файлы должны иметь уникальные имена, потому что pytest
импортирует их как модули верхнего уровня, так как нет пакетов, из которых можно получить полное имя пакета. Другими словами, тестовые файлы в приведенном выше примере будут импортированы как модули верхнего уровня test_app
и test_view
путем добавления tests/
в sys.path
.
Если нужны тестовые модули с одинаковыми именами, то можно добавить файлы __init__.py
в папки и подпапки с тестами, при этом они примут свойства пакетов Python:
pyproject.toml setup.cfg mypkg/ ... tests/ __init__.py foo/ __init__.py test_view.py bar/ __init__.py test_view.py
Теперь pytest
загрузит модули как tests.foo.test_view
и tests.bar.test_view
, что позволит иметь модули с одинаковыми именами. Но теперь это создает тонкую проблему. Чтобы загрузить тестовые модули из каталога tests
, фреймворк pytest
добавляет корень репозитория к sys.path
, что теперь позволяет также импортировать mypkg
.
Это может создавать проблемы, если использовать для тестирования приложения в виртуальной среде такие инструменты, как tox
. В этой ситуации настоятельно рекомендуется использовать макет шаблона src
, в котором корневой пакет приложения находится в подкаталоге корня:
pyproject.toml setup.cfg src/ mypkg/ __init__.py app.py view.py tests/ __init__.py foo/ __init__.py test_view.py bar/ __init__.py test_view.py
Этот макет предотвращает множество распространенных ошибок и имеет много преимуществ.
Примечание: Новый параметр CLI
--import-mode=importlib
(смотрите "Режимы импорта") не имеет перечисленных выше недостатков, посколькуsys.path
не изменяется при импорте тестовых модулей, следовательно пользователям, столкнувшимся с этой проблемой, настоятельно рекомендуется его попробовать.
Встраивание тестовых каталогов в пакет приложения полезно, если есть прямая связь между тестами и модулями приложения и нужно распространять их вместе приложением:
pyproject.toml setup.cfg mypkg/ __init__.py app.py view.py test/ __init__.py test_app.py test_view.py ...
В этой схеме лучше всего запускать тесты с помощью параметра --pyargs
. Например, pytest --pyargs mypkg
. Фреймворк pytest
обнаружит, где установлен mypkg
, и соберет оттуда тесты.
Обратите внимание, что этот макет также работает в сочетании с шаблоном src
, упомянутым в предыдущем разделе.
Примечание.
В режимах импорта (--import-mode
) prepend
и append
, если при рекурсивном обходе проекта pytest
обнаружит тестовый файл 'a/b/test_module.py'
, то имя импорта определяется следующим образом:
baseir
: это первый "восходящий" (по направлению к корню) каталог, не содержащий __init__.py
. Если, например, оба каталога a
и b
содержат файл __init__.py
, тогда родительский каталог a
станет базовым.sys.path.insert(0, basedir)
, это делает тестовый модуль доступным для импорта под полным именем.import a.b.test_module
, где путь определяется путем преобразования разделителей пути /
в .
. Это означает, что нужно сопоставлять именя файлов и директорий импортируемым именам напрямую.Причина этого несколько усовершенствованного метода импорта заключается в том, что в более крупных проектах несколько тестовых модулей могут импортироваться друг из друга, и, таким образом, получение канонического имени импорта помогает избежать неожиданностей, таких как двойной/циклический импорт тестового модуля.
С параметром CLI --import-mode=importlib
все становится менее запутанным, потому что pytest
не нужно менять sys.path
или sys.modules
, что делает вещи гораздо менее удивительными.
pytest
и sys.path/PYTHONPATH
.Фреймворк pytest
в качестве среды тестирования должен импортировать для выполнения тестовые модули и файлы conftest.py
, расположенных в папках с модулями тестов.
Импорт файлов в Python (по крайней мере, до недавнего времени) - нетривиальный процесс, часто требующий изменения sys.path
. Некоторыми аспектами процесса импорта можно управлять с помощью флага командной строки --import-mode
, который может принимать следующие значения:
prepend
(по умолчанию): путь к каталогу, содержащему каждый модуль, будет вставлен в начало sys.path
, а затем импортирован с помощью встроенной функции __import__
.
Когда дерево тестовых каталогов не организовано в пакеты, требуется, чтобы имена тестовых модулей были уникальными, т.к. после импорта модули будут помещены в sys.modules
. Это классический механизм, восходящий к тому времени, когда еще поддерживался Python2.
append
: каталог, содержащий каждый модуль, добавляется в конец sys.path
и импортируется с помощью __import__
.
Это лучше позволяет запускать тестовые модули для установленных версий пакета, даже если тестируемый пакет имеет тот же корень импорта. Например:
testing/__init__.py testing/test_pkg_under_test.py pkg_under_test/
Когда используется CLI --import-mode=append
, то тесты будут выполняться с установленной версией pkg_under_test
, тогда как с помощью prepend
они будут использовать локальную версию. Эта путаница является причиной того, почему лучше использовать макеты src
.
То же, что и prepend
, требуется, чтобы имена тестовых модулей были уникальными, когда дерево каталогов тестов не упорядочено по пакетам, т.к. после импорта модули будут помещены в sys.modules
.
importlib
: новое в pytest-6.0
, этот режим использует importlib
для импорта тестовых модулей. Это дает полный контроль над процессом импорта и не требует изменения sys.path
.
По этой причине это не требует, чтобы имена тестовых модулей были уникальными, но также делает тестовые модули неимпортируемыми друг другом.
В будущих выпусках pytest
значение importlib
для параметра CLI --import-mode
, скорее всего будет значением по умолчанию, в зависимости от отзывов.
prepend
и append
.Список сценариев при использовании режимов импорта prepend
или append
, когда pytest
необходимо изменить sys.path
для импорта тестовых модулей или conftest.py
файлы, а также проблемы, с которыми могут столкнуться пользователи.
pytest
по сравнению с python -m pytest
.Запуск pytest
с помощью pytest [...]
вместо python -m pytest [...]
дает почти эквивалентное поведение, за исключением того, что последний добавит текущий каталог в sys.path
, что является стандартным поведением Python.
Рассмотрим этот макет файла и каталога:
root/ |- foo/ |- __init__.py |- conftest.py |- bar/ |- __init__.py |- tests/ |- __init__.py |- test_foo.py
При выполнении $pytest root/
фреймворк pytest
найдет foo/bar/tests/test_foo.py
и поймет, что это часть пакета, т.к. папке присутствует файл __init__.py
. Затем он будет искать вверх, пока не найдет последнюю папку, которая содержит файл __init__.py
, что будет означать корень пакета (в данном случае foo/
). Чтобы загрузить test_foo.py
как модуль foo.bar.tests.test_foo
, pytest
вставит root/
в начало sys.path
(если он еще не там).
Та же логика применима и к файлу conftest.py
: он будет импортирован как модуль foo.conftest
.
Сохранение полного имени пакета важно, когда тесты находятся в пакете, чтобы избежать проблем и позволить тестовым модулям иметь одинаковые имена.
Рассмотрим этот макет файла и каталога:
root/ |- foo/ |- conftest.py |- bar/ |- tests/ |- test_foo.py
При выполнении $pytest root/
фреймворк pytest
найдет файл foo/bar/tests/test_foo.py
и поймет, что он НЕ является частью пакета, т.к. в той же папке нет файла __init__.py
. Затем он добавит root/foo/bar/tests
в sys.path
, чтобы импортировать test_foo.py
как модуль test_foo
. То же самое он делает с файлом conftest.py
, добавив root/foo
в sys.path
, чтобы импортировать его как conftest
.
По этой причине в этом макете не может быть тестовых модулей с одинаковыми именами, так как все они будут импортированы в глобальное пространство имен.