原文:
towardsdatascience.com/configuring-pytest-to-run-doctest-a4e6b2821043
PYTHON 编程
照片由 Paul Hanaoka 在 Unsplash 提供
现代 Python 项目由 pyproject.toml 文件管理。您可以使用它来管理常规项目和 Python 包,这使得该文件成为设置各种类型 Python 项目的通用工具。
pyproject.toml 文件可以包含项目所需的所有内容,例如:
-
元数据,例如项目名称、版本、作者、许可证、分类器、URL 和描述(包括简短和详细描述)。
-
开发和生产环境的配置,即依赖项和可选依赖项。
-
开发工具的配置,例如
[black](https://github.com/psf/black)、[ruff](https://docs.astral.sh/ruff/)、[pylint](https://github.com/pylint-dev/pylint)、[pytest](https://docs.pytest.org/)以及许多其他工具。
当项目变得庞大时,可以将大量此类信息移动到其他配置文件中,如 pytest.ini、requirements.txt、requirements_dev.txt 等。但当一个 pyproject.toml 文件不是特别长时,我会将所有信息都放在里面——这使得项目的根目录保持小巧。
这只是关于 pyproject.toml 文件的基本信息。如果您想了解更多关于如何使用它的信息,您可以从这里开始:
在本文中,我想讨论一个特定场景,即我们可以使用 pyproject.toml 文件来配置 pytest 以运行 doctest 测试。正如您将看到的,这非常简单。有了这种简单性,您将获得一个非常强大的工具:一个同时适用于 pytest 和 doctest 测试的测试运行器。我还会向您展示如何配置 doctest,包括典型的 doctest 标志以及在文档文件和 Python 脚本中实现的 doctest 测试的运行。
为了展示如何做到这一点,我们将使用一个全新的 Python 包。我将使用 makepackage 包来创建其结构:
然而,我们将进一步配置它以满足我们的需求。
由于我们正在讨论使用 doctest 进行测试,您应该至少掌握该模块的基本知识。您可以在以下文章中了解它:
创建包
我们首先需要创建一个包。为此,让我们创建一个新的虚拟环境,并在其中安装makepackage:
> python -m venv venv-makepackage
> venv-makepackageScriptsactivate
在 Linux 中,上述命令将是:
$ source venv-makepackage/bin/activate
让我们继续:
(venv-makepackage) > makepackage doctest_in_pytest
(venv-makepackage) > deactivate
> cd doctest_in_pytest
> python -m venv .venvScriptsactivate
(.venv) > python -m pip install --upgrade pip
如上所述,我们创建了一个全新的项目,doctest_in_pytest,为其创建了一个新的虚拟环境.venv,激活了它,并升级了pip。
现在,我们需要以可编辑模式安装该包:
(.venv) > python -m pip install -e .[dev]
此命令不仅将安装我们的doctest_in_pytest包,还将所有在dev选项下保留的可选依赖项安装:wheel、black、pytest、mypy、setuptools和build。正如您所看到的,doctest不包含在这个列表中。这是因为它是一个来自 Python 标准库的模块,所以它随着 Python 安装一起安装。
项目的根目录是pyproject.toml文件所在的目录。您将在那里运行pytest命令。
如果您分析项目的当前代码(它是使用makepackage创建的,因此确实有一些占位符代码),您会注意到它结合了pytest单元测试和doctest文档测试。运行前者很简单,只需在根目录中使用以下命令即可:
(.venv) $ pytest
要从 shell 运行doctest,您可以使用以下 shell 命令,也来自项目的根目录:
(.venv) $ python -m doctest README.md doctest_in_pytest/doctest_in_pytest.py
这将使用默认配置运行doctest。然而,通常我们希望使用doctest标志,例如[doctest.ELLIPSIS](https://docs.python.org/3/library/doctest.html#doctest.ELLIPSIS)或[doctest.NORMALIZE_WHITESPACE](https://docs.python.org/3/library/doctest.html#doctest.NORMALIZE_WHITESPACE)。通常我会使用这两个标志以及[doctest.IGNORE_EXCEPTION_DETAIL](https://docs.python.org/3/library/doctest.html#doctest.IGNORE_EXCEPTION_DETAIL)。了解doctest选项标志是很好的,您可以在doctest文档中阅读有关它们的信息。
您可以将它们作为 shell 命令中的 CLI 标志提供,但会相当长:
(.venv) $ python -m doctest README.md doctest_in_pytest/doctest_in_pytest.py -o ELLIPSIS -o NORMALIZE_WHITESPACE -o IGNORE_EXCEPTION_DETAIL
此命令运行位于两个文件中的doctest测试(路径相对于项目的根目录):
-
README.md -
doctest_in_pytest/doctest_in_pytest.py
如果您在多个文件中实现了doctest,您可以将它们包含在上面的命令中。或者,为了使来自不同文件的doctest独立,您可以独立为每个文件调用doctest命令。
下一个部分展示了本文的核心:在pyproject.toml文件中配置pytest,以便您可以使用一个简短的命令运行两种类型的测试,并使用您需要的或偏好的任何配置。命令很简单,只有六个字符的一个单词:
(.venv) $ pytest
在pyproject.toml中配置 doctest 在 pytest
这可能比你想象的要简单,但首先让我们回顾一下我们需要配置什么以让pytest知道我们想要它运行什么类型的命令:
-
存放包含 doctest 测试的文件的目录路径
-
要包含的文件类型
-
Doctest 标志
下面,您将找到一个要包含在pyproject.toml文件中的代码块:
[tool.pytest.ini_options]
testpaths = ["tests", "doctest_in_pytest", "README.md"]
addopts = '--doctest-modules --doctest-glob="*.md"'
doctest_optionflags = [
"ELLIPSIS",
"NORMALIZE_WHITESPACE",
"IGNORE_EXCEPTION_DETAIL"
]
我猜代码是自包含的,但让我们逐行分析它:
[tool.pytest.ini_options]
这是你需要使用来启动pytest配置块的命令行。
testpaths = ["tests", "doctest_in_pytest", "README.md"]
这里,你提供包含doctest测试文件的目录路径。在我们的例子中,这是包含pytest测试的tests/文件夹和包含 doctest 测试的doctest_in_pytest文件夹。
我们可以看到一个例外:README.md文件。它在那里做什么?它在那里是因为无法将根目录包含在testpaths中。如果你包含".",pytest在运行测试时将不会包含文件./README.md。
然而,如果你使用我使用的技巧:提供路径"README.md"。当然,如果你想要包含在doctest测试中的文档测试不在项目根目录中的主README.md文件中,你将不会遇到这个问题。
addopts = '--doctest-modules --doctest-glob="*.md"'
这里,addopts并不代表"adopts",而是"add options"。你可以使用此字段为pytest提供通常作为 CLI 参数传递的选项。
我们使用两个:
-
--doctest-modules: 这意味着pytest应该运行位于 Python 模块中的 doctest 测试,即扩展名为".py"的文件。因此,这不会运行README.md和其他非 Python 模块的doctest,例如*.md、*.rst、*.txt。 -
--doctest-glob="*.md": 这请求pytest在运行doctest时包含所有 Markdown 文件。如果你使用 reStructuredText 格式的README.rst文件,你需要将md改为rst。
doctest_optionflags = [
"ELLIPSIS",
"NORMALIZE_WHITESPACE",
"IGNORE_EXCEPTION_DETAIL"
]
这是一个简单的命令,它为doctest配置选项标志。
就这样!
在行动中
是时候看看我们的项目在实际中的应用了。我将利用使用makepackage创建的新 Python 包中的代码。它将满足我们的需求,因为它包含pytest和doctest测试。然而,由于README.md不包含文档测试,我将显著简化它:
# Configuring `doctest` tests in `pytest`
```python
>>> import doctest_in_pytest as dip
>>> dip.foo(2)
4
>>> dip.foo(100)
10000
>>> dip.bar("无论什么!")
'无论什么!'
>>> dip.bar("AAaa...!")
'aaaa...!'
>>> dip.baz("无论什么!")
'WHATEVER!'
>>> dip.baz("AAaa...!")
'AAAA...!'
```py
在 Markdown 文件中实现 doctests 时,请记住:
-
doctest将运行以>>>提示符开始的代码行。代码是否包含在代码块(如这里)或文本中无关紧要。 -
记得在每个```py`` ending code blocks. If you fail to do so,
doctestwill treat this line as part of the output of the preceding line. So, this code will fail:
```pypython
>>> 2 + 2
4
because `doctest` will see that `2 + 2` evaluates to
```markdown
4
```py
not to
4
因此,你需要将其修改如下:
```python
>>> 2 + 2
4
```py
好的,现在是时候运行我们的测试了。让我们考虑几个典型的用例。
运行特定文件(的)测试
当你想运行特定测试文件时,只需以下方式运行即可:
https://miro.medium.com/1*rQuVvQdcd-5AQsJTtdJliQ.png
我们也可以使用相同的命令来运行 doctests - 只需传递包含 doctests 的文件路径即可。看:
https://miro.medium.com/1*JTH9tkhR4s01MBEhDeVZ7Q.png
当然,你可以在一个命令中使用两个路径:
https://miro.medium.com/1*oqc965wJ-BpGo6cZbYJGdQ.png
如上所示,我们运行了四个 doctests,一个来自README.md文件,三个来自doctest_in_pytest/doctest_in_pytest.py文件。我们也可以这样将doctest与pytest测试结合起来:
https://miro.medium.com/1*Z_pXmohwmtSyX4HGeHgOGQ.png
运行所有测试
使用我们在pyproject.toml文件中的配置,运行所有测试——包括doctest和pytest——非常简单。你只需要一个命令:
https://miro.medium.com/1*AjxjtepyxHjCxQGg9q0RhA.png
就这么简单!
docs/目录中的文档文件
让我们考虑一个更常见的场景,这在数据科学中很常见。让我们在根目录中创建一个docs/目录,我们将用它来保存关于项目和代码的文档文件。让我们添加这样一个文档文件,比如说,docs/examples.md:
# Configuring `doctest` tests in `pytest`
```python
>>> import doctest_in_pytest as dip
>>> dip.foo(2)
4
>>> dip.foo(100)
10000
>>> dip.bar("Whatever!")
'whatever!'
>>> dip.bar("AAaa...!")
'aaaa...!'
>>> dip.baz("Whatever!")
'WHATEVER!'
>>> dip.baz("AAaa...!")
'AAAA...!'
```py
内容对我们来说并不重要;重要的是我们可以运行这个文件的doctest测试,并且测试会通过。
由于文件位于新的目录docs/中,我们需要将其添加到pyproject.toml文件中的testpaths字段。如果不这样做,这个文件将不会被测试,如下所示:
https://miro.medium.com/1*6QZeGyOibmbZ9gIlfGtWPA.png
如你所见,输出没有变化,之前和现在都运行了 15 个测试。因此,让我们通过将docs/目录添加到testpaths来修改pyproject.toml文件:
[tool.pytest.ini_options]
testpaths = ["tests", "doctest_in_pytest", "docs", "README.md"]
addopts = '--doctest-modules --doctest-glob="*.md"'
doctest_optionflags = [
"ELLIPSIS",
"NORMALIZE_WHITESPACE",
"IGNORE_EXCEPTION_DETAIL"
]
我们不需要修改addopts字段,因为新文件也是用 Markdown 编写的。让我们运行测试:
任务完成!
测试数量
有一些有趣——有时也很重要——需要注意和记住的事情。文档文件,如docs/examples.md或README.md,可能包含许多代码行——但pytest会将一个文档文件中的所有doctest行计为一个测试。将代码分成几个代码块不会有所帮助:整个文件仍然只有一个测试,至少当你通过pytest运行doctest时是这样的。
上述计数 doctest 测试的方法适用于文档文件。在 Python 脚本中,你将拥有与带有 doctest 的文档字符串数量一样多的测试。记住这一点,因为当你有长文档文件时,pytest 报告的测试数量可能会变得令人困惑;无论这样的文档文件有多长,它都将被计算为一个 doctest 测试。
因此,不要尝试使用 pytest 的报告来比较 doctest 和 pytest 测试的数量。这可能会令人困惑且具有误导性。
结论
我们已经学会了如何使用 pyproject.toml 文件来配置 pytest 以运行 pytest 和 doctest 测试,这是我项目中发现非常方便的方法。
在此方法之前,我分别处理这些测试并单独运行它们,这变得不方便,尤其是在我想运行所有测试时,比如在创建拉取请求之前。我求助于创建 shell 脚本来使用一个命令运行这两种类型的测试,这有所帮助,但也有其缺点。例如,使用多个标志运行 doctest 命令会使命令变长,所以将其放入脚本中简化了事情。
然而,使用 pytest 命令甚至更简单,且与平台无关。通过 pyproject.toml 文件,我可以配置我需要的所有内容,包括各种 pytest 选项,如 testpaths,以及 doctest 选项,如标志。一旦配置完成,裸 pytest 命令将始终使用此配置。
这种解决方案的另一个优点在共享项目中很明显。由于运行所有测试只需要 pytest 命令,团队中的每个人都会以相同的方式运行相同的测试。否则,如果团队成员选择单独的 doctest 标志,测试可能对某个成员通过,而对另一个成员失败。
我希望你会发现这个简单技巧和我一样有用。如果你像我一样是 doctest 用户,你可能会发现你的工作流程变得更加顺畅。
如果你之前避免使用 doctest 是因为你觉得运行 doctest 会打断你的流畅开发过程,本文讨论的技术将解决你的担忧。从现在开始,运行 doctest 测试不应该带来任何负担。实际上,你甚至不需要再记住运行它们——项目的配置将为你处理这一切。只需记住实现 doctest 并确保它们作为有效的文档测试即可。
在我未来的某篇文章中,我将向你展示一个相关的有用技巧:为 doctest 测试创建 pytest 固定值。
Pytest配置Doctest指南
3172

被折叠的 条评论
为什么被折叠?



