目录
[不要通过 setuptools 运行](#不要通过 setuptools 运行)
[使用 flake8-pytest-style 进行检查](#使用 flake8-pytest-style 进行检查)
使用pip安装包
对于开发,我们建议您使用venv来创建虚拟环境,并使用pip来安装您的应用程序以及任何依赖项,包括pytest包本身。这样做可以确保您的代码和依赖项与系统Python安装保持隔离。
在您的仓库根目录下,按照《Packaging Python Projects》中所述创建一个pyproject.toml
文件。文件的前几行应该像这样:
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "PACKAGENAME"
version = "PACKAGEVERSION"
其中PACKAGENAME
和PACKAGEVERSION
分别代表您的包名和版本号。
然后,您可以从同一个目录以"可编辑"模式安装您的包,运行以下命令:
pip install -e .
这样做可以让您修改源代码(包括测试和应用程序)并随意重新运行测试。
Python测试发现的约定
pytest实现了以下标准的测试发现机制:
- 如果没有指定任何参数,则从testpaths(如果已配置)或当前目录开始收集测试。另外,命令行参数可以以任何目录、文件名或节点ID的组合形式使用。
- 递归遍历目录,除非它们匹配norecursedirs。
- 在这些目录中,搜索以
test_
开头的.py
文件或以_test.py
结尾的文件,并通过它们的测试包名test package name进行导入。 - 从这些文件中收集测试项:
- 类外部以
test
为前缀的测试函数或方法。 - 以
Test
为前缀的测试类(没有__init__
方法)内部的以test
为前缀的测试函数或方法。用@staticmethod
和@classmethod
装饰的方法也会被考虑。
- 类外部以
有关如何自定义测试发现的示例,请参阅更改标准(Python)测试发现Changing standard (Python) test discovery。
在Python模块中,pytest还使用标准的unittest.TestCase子类化技术来发现测试。
选择测试布局
pytest支持两种常见的测试布局:
测试代码放在应用程序代码之外的额外目录
如果你有很多功能测试,或者出于其他原因希望将测试代码与实际应用程序代码分开(这通常是个好主意),那么将测试代码放入实际应用程序代码之外的额外目录可能会很有用:
pyproject.toml
src/
mypkg/
__init__.py
app.py
view.py
tests/
test_app.py
test_view.py
...
这样做有以下好处:
针对已安装版本的测试 :你可以在执行pip install .
之后,针对已安装的版本来运行你的测试。
针对本地副本的可编辑安装测试 :在执行pip install --editable .
之后,你可以针对本地副本的可编辑安装来运行你的测试。这样做的好处是,你可以在不实际安装软件包到系统全局Python环境中的情况下,对软件包进行开发和测试。可编辑模式允许你在源代码位置直接修改代码,并立即在测试或实际应用中看到这些更改的效果。这对于开发过程中快速迭代和调试非常有用。
对于新项目或现有项目,我们推荐使用 importlib
导入模式import mode(详细解释请参考 which-import-mode)。为此,请在您的 pyproject.toml
文件中添加以下内容:
[tool.pytest.ini_options]
addopts = [
"--import-mode=importlib",
]
通常,但特别是当您使用默认的 prepend
导入模式时,我们强烈建议使用 src
布局。在这种布局中,您的应用程序根包位于根目录的子目录中,即 src/mypkg/
而不是 mypkg
。
这种布局可以避免许多常见的陷阱,并带来许多好处,Ionel Cristian Mărieș 在这篇优秀的博客文章blog post中对此进行了更详尽的解释。
注意
如果您不使用可编辑安装(editable install)方式,并且如上所述使用 src
布局,那么您需要扩展 Python 的模块文件搜索路径,以便直接对本地副本执行测试。您可以通过设置 PYTHONPATH
环境变量的方式来临时实现这一点:
PYTHONPATH=src pytest
或者,您可以通过使用pythonpath 配置变量并将其添加到您的 pyproject.toml
文件中来永久实现这一点:
[tool.pytest.ini_options]
pythonpath = "src"
注意
如果您不使用可编辑安装(editable install)方式,并且也不使用 src
布局(即 mypkg
直接位于根目录中),那么您可以依赖 Python 默认将当前目录添加到 sys.path
中的事实来导入您的包,并通过运行 python -m pytest
来直接对本地副本执行测试。
有关调用 pytest
和 python -m pytest
之间的区别的更多信息,请参阅"Invoking pytest versus python -m pytest"。
将测试作为应用程序代码的一部分
如果您的测试与应用程序模块之间存在直接关联,并且您希望将它们与您的应用程序一起分发,那么将测试目录内嵌到您的应用程序包中是非常有用的:
pyproject.toml
[src/]mypkg/
__init__.py
app.py
view.py
tests/
__init__.py
test_app.py
test_view.py
...
在此方案中,使用 --pyargs
选项运行测试非常便捷:
pytest --pyargs mypkg
pytest
将自动发现 mypkg
的安装位置并从那里收集测试。
请注意,此布局也可以与上一节中提到的 src
布局结合使用。
注意
您可以在您的应用程序中使用命名空间包(PEP420),但 pytest
仍然会根据 __init__.py
文件的存在来执行测试包名称的发现test package name。如果您使用上面推荐的两种文件系统布局之一,但从目录中省略了 __init__.py
文件,那么它应该可以正常工作。然而,从"内联测试"中,您需要使用绝对导入来访问您的应用程序代码。
注意
在 prepend
和 append
导入模式下,当 pytest
在遍历文件系统时找到一个名为 "a/b/test_module.py" 的测试文件时,它会按以下方式确定导入名称:
-
确定基准目录(basedir) :这是第一个"向上"(向根目录)且不包含
__init__.py
文件的目录。例如,如果a
和b
都包含__init__.py
文件,则a
的父目录将成为基准目录。 -
修改
sys.path
:执行sys.path.insert(0, basedir)
以确保测试模块可以在完全限定的导入名称下被导入。 -
导入模块 :使用
import a.b.test_module
导入模块,其中路径是通过将路径分隔符/
转换为.
字符来确定的。这意味着您必须遵循目录和文件名直接映射到导入名称的约定。
这种略显复杂的导入技术的原因是,在大型项目中,多个测试模块可能会相互导入,因此推导出规范的导入名称有助于避免诸如测试模块被重复导入等意外情况。
使用 --import-mode=importlib
时,情况就简单多了,因为 pytest
不需要更改 sys.path
或 sys.modules
,这使得事情更加直观。
选择导入模式
由于历史原因,pytest
默认使用 prepend
导入模式,而不是我们推荐给新项目的 importlib
导入模式。这背后的原因在于 prepend
模式的工作方式:
由于无法从包中推导出完整的包名,pytest
会将您的测试文件作为顶级模块进行导入。在第一个示例(src
布局)中的测试文件会被添加到 sys.path
中的 tests/
目录下,并作为 test_app
和 test_view
这样的顶级模块进行导入。
与 importlib
导入模式相比,这会导致一个缺点:您的测试文件必须具有唯一的名称。
如果您需要具有相同名称的测试模块,作为一种解决方法,您可以在测试文件夹和子文件夹中添加 __init__.py
文件,将它们更改为包:
pyproject.toml
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
这样的工具在虚拟环境中测试您的包,那么这将会是一个问题,因为您想要测试的是已安装的包版本,而不是仓库中的本地代码。
importlib
导入模式没有上述任何缺点,因为在导入测试模块时不会更改 sys.path
。
tox
一旦您完成了工作,并希望确保您的实际包通过了所有测试,您可能会想要了解 tox,这是一个虚拟环境测试自动化工具。tox
帮助您设置具有预定义依赖项的虚拟环境,然后执行带有选项的预配置测试命令。它会对已安装的包进行测试,而不是对您的源代码检出进行测试,这有助于检测打包过程中的问题。
tox
的主要优点是它允许您在不同的 Python 版本和解释器环境中运行测试,从而确保您的包在各种环境下都能正常工作。此外,由于它是在已安装的包上运行测试,因此它更接近于模拟用户实际使用包的情况,这有助于识别任何与打包或分发相关的问题。
使用 tox
,您可以在项目的根目录中定义一个 tox.ini
文件,该文件指定了测试环境、依赖项和要运行的测试命令。然后,您可以通过简单地运行 tox
命令来自动化测试过程,该命令会为您在 tox.ini
文件中定义的每个环境创建虚拟环境,安装必要的依赖项,并运行测试。
不要通过 setuptools 运行
不建议将 pytest 与 setuptools 集成,即您不应该使用 python setup.py test
或 pytest-runner
,并且这些功能在未来可能会停止工作。
这是不推荐的做法,因为它依赖于 setuptools 中已弃用的功能,并且依赖于会破坏 pip 中安全机制的特性。例如,setup_requires
和 tests_require
会绕过 pip 的 --require-hashes
参数。有关更多信息和迁移说明,请参阅 pytest-runner 的通知pytest-runner notice。同时,也可以查看 pypa/setuptools#1684 了解更多关于 setuptools 计划移除测试命令的信息。
简而言之,为了保持测试的稳定性和安全性,建议使用更现代的测试方法来替代通过 setuptools 运行测试的做法remove the test command。
使用 flake8-pytest-style 进行检查
为了确保 pytest 在您的项目中被正确使用,使用 flake8-pytest-style这个 flake8 插件会很有帮助。
flake8-pytest-style 检查 pytest 代码中常见的错误和编码风格违规,比如夹具(fixtures)的错误使用、测试函数名称不当、以及标记(markers)的错误使用等。通过使用这个插件,您可以在开发过程的早期阶段捕获这些错误,并确保您的 pytest 代码保持一致性且易于维护。
flake8-pytest-style 检测到的 lint 列表可以在其 PyPI page 页面上找到。
注意
flake8-pytest-style 不是官方的 pytest 项目。其中一些规则强制执行特定的风格选择,例如使用 @pytest.fixture()
而不是 @pytest.fixture
,但您可以配置该插件以符合您偏好的风格。