pytest 导入机制和 sys.path / PYTHONPATH

pytest import mechanisms and sys.path / PYTHONPATH

导入模式

Import modes

pytest 作为一个测试框架,需要导入测试模块和 conftest.py 文件以进行执行。

在 Python 中导入文件是一个非平凡的过程,因此可以通过 --import-mode 命令行标志控制导入过程的某些方面,该标志可以取以下值:

  • prepend (默认值):如果模块所在的目录路径尚未在 sys.path 中,则将其插入到 开始 位置,然后使用 importlib.import_module 函数进行导入。

    强烈建议通过向包含测试的目录添加 __init__.py 文件,将测试模块安排为包。这将使测试成为一个合适的 Python 包的一部分,从而允许 pytest 解析它们的完整名称(例如,tests.core.test_core 对应于 tests/core/test_core.py)。

    如果测试目录树没有按包的方式组织,那么每个测试文件需要与其他测试文件具有唯一名称,否则 pytest 会在发现两个具有相同名称的测试时引发错误。

    这是经典机制,追溯到 Python 2 仍在支持的时代。

  • append :如果模块所在的目录尚未在 sys.path 中,则将其附加到 sys.path 的末尾,并使用 importlib.import_module 进行导入。

    这更好地允许用户在包的安装版本上运行测试模块,即使被测试的包具有相同的导入根。例如:

    testing/__init__.py
    testing/test_pkg_under_test.py
    pkg_under_test/
    

    当使用 --import-mode=append 时,测试将针对 pkg_under_test 的已安装版本运行,而在使用 prepend 时,它们会选择本地版本。这种混淆是我们倡导使用 src-layouts 的原因。

    prepend 一样,当测试目录树未以包的方式组织时,需要测试模块名称唯一,因为导入后模块将被放入 sys.modules

  • importlib :此模式使用 importlib 提供的更细粒度的控制机制导入测试模块,而不改变 sys.path

    此模式的优点:

    • pytest 不会更改 sys.path

    • 测试模块名称不需要唯一——pytest 将根据 rootdir 自动生成唯一名称。

    缺点:

    • 测试模块不能相互导入。

    • 测试目录中的测试工具模块(例如,包含与测试相关的函数/类的 tests.helpers 模块)不可导入。在这种情况下,建议将测试工具模块与应用程序/库代码放在一起,例如 app.testing.helpers

    重要提示:我们所指的 “测试工具模块” 是指被其他测试直接导入的函数/类;这不包括 fixtures,fixtures 应放在 conftest.py 文件中,并与测试模块一起自动被 pytest 发现。

    工作原理如下:

    1. 给定某个模块路径,例如 tests/core/test_models.py,推导出规范名称如 tests.core.test_models 并尝试导入它。

      对于非测试模块,如果它们可以通过 sys.path 访问,则此步骤将成功。因此,例如,.env/lib/site-packages/app/core.py 将作为 app.core 可导入。这在插件导入非测试模块(例如,文档测试)时发生。

      如果此步骤成功,模块将被返回。

      对于测试模块,除非它们可以从 sys.path 访问,否则此步骤将失败。

    2. 如果上一步失败,我们使用 importlib 的功能直接导入模块,这使我们能够在不改变 sys.path 的情况下导入它。

      由于 Python 要求模块也在 sys.modules 中可用,pytest 根据其相对于 rootdir 的位置为其推导出一个唯一名称,并将该模块添加到 sys.modules

      例如,tests/core/test_models.py 将作为模块 tests.core.test_models 被导入。

Added in version 6.0.

Note

起初,我们打算在未来版本中将 importlib 设为默认值,然而现在显然它有自己的一系列缺点,因此默认值将继续保持为 prepend,至少在可预见的未来。

Note

默认情况下,pytest 不会自动尝试解析命名空间包,但可以通过 consider_namespace_packages 配置变量进行更改。

See also

pythonpath 配置变量。

consider_namespace_packages 配置变量。

测试布局

prependappend 导入模式场景

prepend and append import modes scenarios

以下是使用 prependappend 导入模式时,pytest 需要更改 sys.path 以导入测试模块或 conftest.py 文件的场景列表,以及用户可能因此遇到的问题。

包内的测试模块/conftest.py 文件

Test modules / ``conftest.py`` files inside packages

考虑以下文件和目录布局:

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/``)。为了加载该模块,它会将 ``root/`` 插入到 :py:data:`sys.path` 的前面(如果还没有在那里),以便将 ``test_foo.py`` 加载为 *模块 foo.bar.tests.test_foo

相同的逻辑适用于 conftest.py 文件:它将被导入为 foo.conftest 模块。

在测试位于包中时,保持完整的包名是重要的,以避免问题并允许测试模块具有重复的名称。这在 测试发现 中也有详细讨论。

独立测试模块/ conftest.py 文件

Standalone test modules / conftest.py files

考虑以下文件和目录布局:

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

因此,这种布局不能有相同名称的测试模块,因为它们都会在全局导入命名空间中被导入。

这在 测试发现 中也有详细讨论。

调用 pytestpython -m pytest

Invoking pytest versus python -m pytest

使用 pytest [...] 运行 pytest 而不是 python -m pytest [...] 会产生几乎等效的行为,唯一的区别是后者会将当前目录添加到 sys.path 中,这符合标准的 python 行为。

另见 通过 python -m pytest 调用 pytest