如何运行 doctest

默认情况下,所有与 test*.txt 模式匹配的文件都将通过 Python 标准 doctest 模块运行。您可以通过发出以下命令来更改模式

pytest --doctest-glob="*.rst"

在命令行中。 --doctest-glob 可以多次出现在命令行中。

如果您有一个这样的文本文件

# content of test_example.txt

hello this is a doctest
>>> x = 3
>>> x
3

那么您可以直接调用 pytest

$ pytest
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-8.x.y, pluggy-1.x.y
rootdir: /home/sweet/project
collected 1 item

test_example.txt .                                                   [100%]

============================ 1 passed in 0.12s =============================

默认情况下,pytest 将收集 test*.txt 文件以查找 doctest 指令,但您可以使用 --doctest-glob 选项(允许多次)传递其他 glob。

除了文本文件之外,您还可以直接从类和函数的文档字符串中执行 doctest,包括从测试模块中执行 doctest

# content of mymodule.py
def something():
    """a doctest in a docstring
    >>> something()
    42
    """
    return 42
$ pytest --doctest-modules
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-8.x.y, pluggy-1.x.y
rootdir: /home/sweet/project
collected 2 items

mymodule.py .                                                        [ 50%]
test_example.txt .                                                   [100%]

============================ 2 passed in 0.12s =============================

您可以通过将这些更改放入 pytest.ini 文件中来使它们在您的项目中永久生效,如下所示

# content of pytest.ini
[pytest]
addopts = --doctest-modules

编码

默认编码为 UTF-8,但您可以使用 doctest_encoding ini 选项指定将用于这些 doctest 文件的编码

# content of pytest.ini
[pytest]
doctest_encoding = latin1

使用“doctest”选项

Python 的标准 doctest 模块提供了一些 选项 来配置 doctest 测试的严格性。在 pytest 中,您可以使用配置文件启用这些标志。

例如,要让 pytest 忽略尾随空格并忽略冗长的异常堆栈跟踪,您可以直接编写

[pytest]
doctest_optionflags = NORMALIZE_WHITESPACE IGNORE_EXCEPTION_DETAIL

或者,可以在文档测试本身的内联注释中启用选项

>>> something_that_raises()  # doctest: +IGNORE_EXCEPTION_DETAIL
Traceback (most recent call last):
ValueError: ...

pytest 还引入了新选项

  • ALLOW_UNICODE:启用后,将从预期的 doctest 输出中剥离 unicode 字符串中的 u 前缀。这允许 doctest 在 Python 2 和 Python 3 中不变地运行。

  • ALLOW_BYTES:类似地,将从预期的 doctest 输出中剥离字节字符串中的 b 前缀。

  • NUMBER:启用后,浮点数只需要与您在预期的 doctest 输出中编写的精度相匹配。使用 pytest.approx() 比较数字,相对容差等于精度。例如,在将 3.14pytest.approx(math.pi, rel=10**-2) 比较时,以下输出只需要匹配到小数点后两位

    >>> math.pi
    3.14
    

    如果您编写 3.1416,那么实际输出需要匹配到小数点后大约四位;依此类推。

    这避免了由有限的浮点数精度引起的误报,如下所示

    Expected:
        0.233
    Got:
        0.23300000000000001
    

    NUMBER 还支持浮点数列表——实际上,它匹配出现在输出中的任何位置的浮点数,即使是在字符串中!这意味着在配置文件的 doctest_optionflags 中全局启用它可能不合适。

    在 5.1 版本中添加。

在失败时继续

默认情况下,pytest 仅报告给定 doctest 的第一个失败。如果您希望在发生失败时继续测试,请执行

pytest --doctest-modules --doctest-continue-on-failure

输出格式

您可以使用选项中的标准 doctest 模块格式之一来更改 doctest 失败时的 diff 输出格式(参见 doctest.REPORT_UDIFFdoctest.REPORT_CDIFFdoctest.REPORT_NDIFFdoctest.REPORT_ONLY_FIRST_FAILURE

pytest --doctest-modules --doctest-report none
pytest --doctest-modules --doctest-report udiff
pytest --doctest-modules --doctest-report cdiff
pytest --doctest-modules --doctest-report ndiff
pytest --doctest-modules --doctest-report only_first_failure

pytest 特有功能

提供了一些功能,以便更轻松地编写 doctest 或更好地与您现有的测试套件集成。但是,请记住,通过使用这些功能,您的 doctest 将与标准 doctests 模块不兼容。

使用 fixture

可以使用 getfixture 帮助程序使用 fixture

# content of example.rst
>>> tmp = getfixture('tmp_path')
>>> ...
>>>

请注意,fixture 需要在 pytest 可见的位置定义,例如 conftest.py 文件或插件;通常不会扫描包含文档字符串的普通 python 文件以查找 fixture,除非通过 python_files 显式配置。

此外,在执行文本 doctest 文件时支持 usefixtures 标记和标记为 autouse 的 fixture。

‘doctest_namespace’fixture

可以使用 doctest_namespace fixture 将项注入到 doctest 运行的命名空间中。它旨在在您自己的 fixture 中使用,以便为使用它们的测试提供上下文。

doctest_namespace 是一个标准 dict 对象,您将要出现在 doctest 命名空间中的对象放入其中

# content of conftest.py
import pytest
import numpy


@pytest.fixture(autouse=True)
def add_np(doctest_namespace):
    doctest_namespace["np"] = numpy

然后可以在您的 doctest 中直接使用它

# content of numpy.py
def arange():
    """
    >>> a = np.arange(10)
    >>> len(a)
    10
    """

请注意,与普通的 conftest.py 一样,fixture 是在 conftest 所在的目录树中发现的。这意味着如果您将 doctest 与源代码放在一起,则相关的 conftest.py 需要在同一目录树中。不会在同级目录树中发现 fixture!

跳过测试

出于可能希望跳过普通测试的原因,也可以跳过 doctest 中的测试。

要跳过 doctest 中的单个检查,可以使用标准 doctest.SKIP 指令

def test_random(y):
    """
    >>> random.random()  # doctest: +SKIP
    0.156231223

    >>> 1 + 1
    2
    """

这将跳过第一个检查,但不会跳过第二个检查。

pytest 还允许在 doctest 中使用标准的 pytest 函数 pytest.skip()pytest.xfail(),这可能很有用,因为你可以根据外部条件跳过/失败测试

>>> import sys, pytest
>>> if sys.platform.startswith('win'):
...     pytest.skip('this doctest does not work on Windows')
...
>>> import fcntl
>>> ...

但是不鼓励使用这些函数,因为它会降低文档字符串的可读性。

注意

pytest.skip()pytest.xfail() 的行为有所不同,具体取决于 doctest 是在 Python 文件(在文档字符串中)还是在包含 doctest 和文本的文本文件中

  • Python 模块(文档字符串):这些函数仅在该特定文档字符串中起作用,让同一模块中的其他文档字符串正常执行。

  • 文本文件:这些函数将跳过/失败整个文件的其余部分的检查。

替代方案

虽然内置的 pytest 支持为使用 doctest 提供了一组很好的功能,但如果你广泛使用它们,你可能会对那些添加了许多更多功能并包括 pytest 集成的外部包感兴趣

  • pytest-doctestplus:提供高级 doctest 支持,并支持测试 reStructuredText (“.rst”) 文件。

  • Sybil:提供了一种通过从文档源解析示例并在正常测试运行中评估解析的示例来测试文档中示例的方法。