良好的集成实践

使用 pip 安装包

在开发时,我们建议使用 venv 创建虚拟环境,并使用 pip 安装你的应用程序及其依赖项,以及 pytest 包本身。这能确保你的代码和依赖项与系统安装的 Python 环境隔离开来。

按照 打包 Python 项目 中的说明,在仓库根目录下创建一个 pyproject.toml 文件。前几行应该如下所示

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "PACKAGENAME"
version = "PACKAGEVERSION"

其中 PACKAGENAMEPACKAGEVERSION 分别是你的包名和版本号。

然后,你可以通过在同一目录下运行以下命令,以“可编辑”模式安装你的包:

pip install -e .

这允许你随意修改源代码(测试和应用程序)并随时重新运行测试。

Python 测试发现约定

pytest 实现了以下标准测试发现机制

  • 如果没有指定参数,则从 testpaths(如果已配置)或当前目录开始收集。或者,可以使用目录、文件名或节点 ID 的任意组合作为命令行参数。

  • 递归进入目录,除非它们匹配 norecursedirs

  • 在这些目录中,搜索 test_*.py*_test.py 文件,并根据它们的测试包名进行导入。

  • 从这些文件中收集测试项

    • 类之外的 test 前缀的测试函数或方法。

    • Test 前缀的测试类(不包含 __init__ 方法)中 test 前缀的测试函数或方法。被 @staticmethod@classmethods 装饰的方法也会被考虑在内。

有关如何自定义测试发现的示例,请参阅 更改标准(Python)测试发现

在 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 . 后,你的测试可以针对具有可编辑安装的本地副本运行。

对于新项目,我们建议使用 importlib 导入模式(详见 which-import-mode)。为此,请在配置文件中添加以下内容

# content of pytest.toml
[pytest]
addopts = ["--import-mode=importlib"]

通常,特别是如果你使用默认的 prepend 导入模式,强烈建议使用 src 布局。在这种布局中,你的应用程序根包位于根目录的子目录中,即 src/mypkg/ 而不是 mypkg

这种布局可以防止许多常见的陷阱并具有许多优点,Ionel Cristian Mărieș 在这篇出色的 博文 中对此有更好的解释。

注意

如果你不使用可编辑安装,并像上面那样使用 src 布局,则需要扩展 Python 的模块文件搜索路径,以便直接针对本地副本执行测试。你可以通过设置 PYTHONPATH 环境变量以 ad-hoc 方式实现

PYTHONPATH=src pytest

或者通过使用 pythonpath 配置变量并向配置文件添加以下内容来实现持久化

[pytest]
pythonpath = ["src"]
[pytest]
pythonpath = src

注意

如果你不使用可编辑安装且不使用 src 布局(mypkg 直接在根目录中),则可以利用 Python 默认将当前目录放入 sys.path 以导入你的包这一事实,并运行 python -m pytest 直接针对本地副本执行测试。

有关调用 pytestpython -m pytest 之间区别的更多信息,请参阅 调用 pytest 与 python -m pytest

另请参见

src 布局 vs 平铺布局

《Python 打包用户指南》讨论了 src 布局和 flat(平铺)布局之间的权衡。

作为应用程序代码一部分的测试

如果你在测试和应用程序模块之间有直接关系,并希望将它们与应用程序一起分发,那么将测试目录内联到应用程序包中非常有用。

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 文件的存在执行测试包名发现。如果你使用了上述两种推荐的文件系统布局之一,但从目录中省略了 __init__.py 文件,它也应该能正常工作。但是,对于“内联测试”,你需要使用绝对导入来获取你的应用程序代码。

注意

prependappend 导入模式下,如果 pytest 在递归进入文件系统时发现了一个 "a/b/test_module.py" 测试文件,它将按如下方式确定导入名称

  • 确定 basedir:这是第一个不包含 __init__.py 的“向上”(朝向根目录)路径的目录。例如,如果 ab 都包含 __init__.py 文件,那么 a 的父目录将成为 basedir

  • 执行 sys.path.insert(0, basedir),使测试模块能够以全限定导入名进行导入。

  • import a.b.test_module,其中路径是通过将路径分隔符 / 转换为“.”字符来确定的。这意味着你必须遵循目录名和文件名直接映射到导入名称的约定。

这种相对复杂的导入技术的原因是,在大型项目中,多个测试模块可能会相互导入,因此派生一个规范的导入名称有助于避免诸如测试模块被导入两次之类的意外情况。

使用 --import-mode=importlib 后,情况就不那么复杂了,因为 pytest 不需要更改 sys.path,这使得事情变得非常直观。

选择导入模式

出于历史原因,pytest 默认使用 prepend 导入模式,而不是我们为新项目推荐的 importlib 导入模式。原因在于 prepend 模式的工作方式

由于没有包可以从中导出完整的包名,pytest 会将你的测试文件作为“顶层”模块导入。第一个示例(src 布局)中的测试文件将通过将 tests/ 添加到 sys.path 中,作为 test_apptest_view 顶层模块导入。

importlib 导入模式相比,这有一个缺点:你的测试文件必须有唯一的名称

如果你需要具有相同名称的测试模块,作为一种变通方法,你可以在你的 tests 目录和子目录中添加 __init__.py 文件,将它们更改为包

pyproject.toml
mypkg/
    ...
tests/
    __init__.py
    foo/
        __init__.py
        test_view.py
    bar/
        __init__.py
        test_view.py

现在 pytest 将把模块加载为 tests.foo.test_viewtests.bar.test_view,允许你拥有同名的模块。但现在这引入了一个微妙的问题:为了从 tests 目录加载测试模块,pytest 会将仓库的根目录添加到 sys.path 中,这会带来一个副作用:现在 mypkg 也可以被导入了。

如果你使用像 tox 这样的工具在虚拟环境中测试你的包,这会很麻烦,因为你希望测试的是已安装版本的包,而不是仓库中的本地代码。

importlib 导入模式没有任何上述缺点,因为在导入测试模块时不会更改 sys.path

tox

一旦完成了工作并想要确保你的实际包通过了所有测试,你可能需要了解一下 tox,这是一个虚拟环境测试自动化工具。tox 可以帮助你设置具有预定义依赖项的虚拟环境,然后执行带有选项的预配置测试命令。它将针对已安装的包运行测试,而不是针对你的源代码检出运行,这有助于发现打包故障。

不要通过 setuptools 运行

不推荐与 setuptools 集成,即你不应该使用 python setup.py testpytest-runner,这些方法在未来可能会停止工作。

这是被弃用的,因为它依赖于 setuptools 的弃用特性,并且依赖于破坏 pip 安全机制的特性。例如,‘setup_requires’ 和 ‘tests_require’ 会绕过 pip --require-hashes。有关更多信息和迁移说明,请参阅 pytest-runner 通知。另请参阅 pypa/setuptools#1684

setuptools 打算 移除 test 命令

使用 flake8-pytest-style 进行检查

为了确保在项目中正确使用 pytest,使用 flake8-pytest-style flake8 插件会很有帮助。

flake8-pytest-style 会检查 pytest 代码中的常见错误和编码风格违规,例如错误地使用 fixture、测试函数名和标记。通过使用此插件,你可以在开发过程中尽早发现这些错误,并确保你的 pytest 代码一致且易于维护。

flake8-pytest-style 检测到的代码检查列表可以在其 PyPI 页面 上找到。

注意

flake8-pytest-style 不是官方的 pytest 项目。有些规则强制执行特定的风格选择,例如使用 @pytest.fixture() 而不是 @pytest.fixture,但你可以配置该插件以符合你偏好的风格。

使用 pytest 的严格模式

9.0 版本新增。

Pytest 包含一组使配置更加严格的选项。出于兼容性或其他原因,这些选项默认是关闭的,但如果可以,你应该启用它们。

你可以通过设置 strict 配置选项一次性启用所有严格性选项

[pytest]
strict = true
[pytest]
strict = true

有关它启用的选项及其效果,请参阅 strict 文档。

如果 pytest 将来添加了新的严格性选项,它们也会在严格模式下启用。因此,你应该仅在固定/锁定 pytest 版本时,或者希望在添加新严格性选项时主动采用它们时,才启用严格模式。如果你不想自动获取新选项,可以单独启用选项

[pytest]
strict_config = true
strict_markers = true
strict_parametrization_ids = true
strict_xfail = true
[pytest]
strict_config = true
strict_markers = true
strict_parametrization_ids = true
strict_xfail = true

如果你想使用严格模式,但在某个特定选项上遇到问题,你可以单独将其关闭

[pytest]
strict = true
strict_parametrization_ids = false
[pytest]
strict = true
strict_parametrization_ids = false