Сообщить об ошибке.

Интеграция с проектом тестов pytest в Python

Надлежащая практика интеграции pytest с проектом

Содержание:


Для разработки и интеграции с проектом 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.
  • если не используется локальная копия приложения. Принимая во внимание тот факт что, для импорта проекта, Python по умолчанию поместит текущий каталог в 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.

По этой причине в этом макете не может быть тестовых модулей с одинаковыми именами, так как все они будут импортированы в глобальное пространство имен.