pytest 导入机制和 sys.path/PYTHONPATH

导入模式

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

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

  • prepend(默认):如果尚未存在,将包含每个模块的目录路径插入到 sys.path开头,然后使用 importlib.import_module 函数导入。

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

    如果测试目录树没有安排为包,那么每个测试文件与其他测试文件相比需要具有唯一的名称,否则如果 pytest 发现两个具有相同名称的测试,它将引发错误。

    这是经典机制,可以追溯到 Python 2 仍然受支持的时候。

  • append:如果尚未存在,将包含每个模块的目录追加到 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

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

    它的工作原理如下

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

      对于非测试模块,如果可以通过 sys.path 访问它们,这将起作用,因此例如 .env/lib/site-packages/app/core.py 可以作为 app.core 导入。当插件导入非测试模块(例如 doctesting)时,就会发生这种情况。

      如果此步骤成功,则返回该模块。

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

    2. 如果上一步失败,我们将使用 importlib 设施直接导入模块,这使我们可以在不更改 sys.path 的情况下导入它。

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

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

    在版本 6.0 中添加。

注意

最初我们打算在未来版本中将 importlib 设为默认值,但现在很明显它有自己的一系列缺点,因此在可预见的未来,默认值仍将是 prepend

注意

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

另请参阅

pythonpath 配置变量。

consider_namespace_packages 配置变量。

选择测试布局.

prependappend 导入模式场景

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

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

考虑此文件和目录布局

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/ 插入到 sys.path 的前面(如果尚未存在),以便将 test_foo.py 作为模块 foo.bar.tests.test_foo 加载。

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

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

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

考虑此文件和目录布局

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 导入。通过将 root/foo 添加到 sys.path 中,对 conftest.py 文件执行相同的操作,以便将其作为 conftest 导入。

因此,此布局不能具有同名的测试模块,因为它们都将在全局导入名称空间中导入。

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

调用 pytestpython -m pytest

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

另请参阅 通过 python -m pytest 调用 pytest