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.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-layouts 的原因。与
prepend
相同,当测试目录树未安排在包中时,需要测试模块名称是唯一的,因为模块在导入后将放入sys.modules
中。
importlib
: 此模式使用importlib
提供的更精细的控制机制来导入测试模块,而无需更改sys.path
。此模式的优点
pytest 完全不会更改
sys.path
。测试模块名称不需要是唯一的 - pytest 将基于
rootdir
自动生成唯一的名称。
缺点
测试模块不能相互导入。
测试目录中的测试实用程序模块(例如,包含与测试相关的功能/类的
tests.helpers
模块)是不可导入的。在这种情况下,建议将测试实用程序模块与应用程序/库代码放在一起,例如app.testing.helpers
。重要提示:通过“测试实用程序模块”,我们指的是其他测试直接导入的功能/类;这不包括 fixtures,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 [...]
运行 pytest 会产生几乎等效的行为,除了后者会将当前目录添加到 sys.path
,这是标准的 python
行为。