pytest 导入机制和 sys.path/PYTHONPATH¶
导入模式¶
pytest 作为测试框架,需要导入测试模块和 conftest.py 文件以执行。
在 Python 中导入文件是一个复杂的过程,因此可以通过 --import-mode 命令行标志来控制导入过程的各个方面,它可以取以下值:
prepend(默认): 包含每个模块的目录路径将被插入到sys.path的 开头 (如果尚未存在),然后使用importlib.import_module函数导入。强烈建议通过在包含测试的目录中添加
__init__.py文件,将测试模块组织为包。这将使测试成为一个合适的 Python 包的一部分,允许 pytest 解析它们的完整名称(例如,对于tests.core包中的test_core.py,其完整名称为tests.core.test_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-layout 的原因。与
prepend相同,当测试目录树未组织成包时,要求测试模块名称唯一,因为模块在导入后将被放入sys.modules。
importlib: 此模式使用importlib提供的更精细的控制机制来导入测试模块,而不改变sys.path。此模式的优点
pytest 完全不会改变
sys.path。测试模块名称无需唯一——pytest 将根据
rootdir自动生成一个唯一的名称。
缺点
测试模块无法相互导入。
测试目录中的测试实用程序模块(例如,包含测试相关函数/类的
tests.helpers模块)不可导入。在这种情况下,建议将测试实用程序模块与应用程序/库代码放在一起,例如app.testing.helpers。重要提示:“测试实用程序模块”指的是被其他测试直接导入的函数/类;这不包括固件(fixtures),固件应该与测试模块一起放在
conftest.py文件中,并由 pytest 自动发现。
它的工作原理如下:
给定一个特定的模块路径,例如
tests/core/test_models.py,它会派生出一个规范名称,例如tests.core.test_models,并尝试导入它。对于非测试模块,如果它们可以通过
sys.path访问,这将起作用。例如,.env/lib/site-packages/app/core.py将可以作为app.core导入。当插件导入非测试模块时(例如 doctesting),就会发生这种情况。如果此步骤成功,则返回该模块。
对于测试模块,除非它们可以从
sys.path访问,否则此步骤将失败。如果上一步失败,我们将直接使用
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 配置变量进行更改。
prepend 和 append 导入模式场景¶
以下是使用 prepend 或 append 导入模式的场景列表,其中 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 导入。对于 conftest.py 文件也同样处理,通过将 root/foo 添加到 sys.path 来将其作为 conftest 导入。
因此,这种布局不能包含同名的测试模块,因为它们都将被导入到全局导入命名空间中。
这在 Python 测试发现约定 中也有详细讨论。
调用 pytest 与 python -m pytest¶
运行 pytest [...] 而不是 python -m pytest [...] 产生几乎相同的行为,不同之处在于后者会将当前目录添加到 sys.path,这是标准的 python 行为。