目录
默认情况下,所有符合 test*.txt
模式的文件都将通过 Python 的标准 doctest 模块运行。您可以通过发出以下命令来改变这个模式:
pytest --doctest-glob="*.rst"
这条命令会告诉 pytest 运行所有符合 *.rst
(即所有以 .rst
结尾的文件)模式的文件的 doctests。这样,您就可以让 pytest 运行使用 ReStructuredText(RST)编写的文档中的 doctests,而不仅仅是传统的文本文件。
在命令行上,--doctest-glob
参数可以多次给出。
如果你有一个像这样的文本文件:
# 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
选项(该选项可以多次使用)来传递额外的模式。这样做可以让你更灵活地指定哪些文件应该包含 doctests。
除了文本文件之外,你还可以直接从你的类和函数的文档字符串(docstrings)中执行 doctests,包括从测试模块中执行。这意味着,只要你的函数或类的文档字符串中包含了以 >>>
开头的代码示例,pytest 就可以通过 --doctest-modules
选项来执行这些示例,并将它们视为测试用例。
下面是一个示例,展示了如何在 Python 模块中使用 doctests:
# mymodule.py 的内容
def something():
"""
一个位于文档字符串中的 doctest
>>> 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 --doctest-modules
命令收集了两个测试项:一个是 mymodule.py
模块中的 doctests,另一个是之前提到的 test_example.txt
文件中的 doctests。
你可以通过将这些更改放入一个名为 pytest.ini
的文件中来使它们在你的项目中永久生效。
# pytest.ini 的内容
[pytest]
addopts = --doctest-modules
编码
默认编码是 UTF-8,但你可以使用 doctest_encoding
ini 选项来指定这些 doctest 文件将使用的编码。
下面是一个 pytest.ini
文件的示例,展示了如何设置 doctest_encoding
为 latin1
:
# pytest.ini 的内容
[pytest]
doctest_encoding = latin1
这样配置后,当你在项目中运行 pytest 时,它会使用 latin1
编码来读取和执行 doctest 文件中的代码和注释。这对于处理非 UTF-8 编码的文本文件特别有用,例如某些旧系统或特定区域设置中生成的文档。
使用‘doctest’选项
Python 的标准 doctest
模块提供了一些选项来配置 doctest 测试的严格性。在 pytest 中,你可以通过配置文件来启用这些标志。
例如,为了让 pytest 忽略尾随空白字符并忽略冗长的异常堆栈跟踪,你可以在配置文件中这样写:
[pytest]
doctest_optionflags = NORMALIZE_WHITESPACE IGNORE_EXCEPTION_DETAIL
这样,pytest 在执行 doctests 时会忽略文档字符串中的尾随空格,并且在遇到异常时不会显示详细的堆栈跟踪,只会显示异常类型和简短的消息(如果可用)。
另外,你也可以在 doctest 本身的文档字符串中使用内联注释来启用选项:
>>> something_that_raises() # doctest: +IGNORE_EXCEPTION_DETAIL
Traceback (most recent call last):
ValueError: ...
这个例子中,# doctest: +IGNORE_EXCEPTION_DETAIL
注释告诉 doctest 忽略 something_that_raises()
函数调用引发的异常的详细堆栈跟踪。这在你只关心异常类型而不关心具体堆栈跟踪时非常有用。请注意,内联注释后面紧跟着的异常示例(如 ValueError: ...
)应该符合你的测试需求,但在这里使用省略号(...
)来表示我们不对异常消息的具体内容进行验证。
pytest 还引入了新的选项:
ALLOW_UNICODE
:启用此选项后,在预期的 doctest 输出中,Unicode 字符串前面的 u
前缀将被移除。这允许 doctests 在 Python 2 和 Python 3 中无需更改即可运行。由于 Python 3 中字符串默认就是 Unicode,这个选项主要用于确保在两种 Python 版本中测试的一致性。
ALLOW_BYTES
:类似地,此选项会从预期的 doctest 输出中的字节字符串中移除 b
前缀。这有助于编写与 Python 2 和 Python 3 兼容的 doctests,因为 Python 3 在处理字节和字符串时比 Python 2 更为严格。
NUMBER
:启用此选项后,浮点数的比较将仅匹配到你在预期 doctest 输出中编写的精度。这些数字的比较将使用 pytest.approx()
进行,其相对容差等于你指定的精度。例如,当将 3.14
与 pytest.approx(math.pi, rel=10**-2)
进行比较时,以下输出只需匹配到小数点后两位即可。
>>math.pi
3.14
如果你写的是3.1416,那么实际输出需要匹配到大约4位小数;以此类推。
这样做可以避免由于浮点数精度有限而导致的误报,比如下面这种情况:
Expected:
0.233
Got:
0.23300000000000001
NUMBER
也支持浮点数列表——实际上,它匹配输出中任何位置的浮点数,即使在字符串内部!这意味着在你的配置文件中全局启用doctest_optionflags
可能并不合适。
此功能在版本5.1中增加。
失败继续
默认情况下,pytest 只会报告给定 doctest 的第一个失败。如果你希望在遇到失败时继续执行测试,可以这样做:
pytest --doctest-modules --doctest-continue-on-failure
这个命令会告诉 pytest 即使遇到失败也继续执行 doctest 模块中的测试。
输出格式
你可以通过在选项中使用标准 doctest 模块格式之一来改变 doctest 在失败时的差异输出格式(请参阅doctest.REPORT_UDIFF, doctest.REPORT_CDIFF, doctest.REPORT_NDIFF, doctest.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 # 仅显示第一个失败的差异
这些选项允许你控制当 doctest 失败时,pytest 如何展示差异信息,从而帮助你更容易地理解和调试问题。
pytest-specific features
为了更容易地编写doctest或更好地与现有测试套件集成,提供了一些特定功能。但请注意,使用这些功能将使你的doctest与标准doctest模块不兼容。
使用fixture
你可以使用getfixture
辅助函数来帮助使用fixture:
# example.rst 文件内容
>>> tmp = getfixture('tmp_path')
>>> ...
>>>
请注意,fixture需要在pytest可见的地方定义,例如,在conftest.py
文件或插件中;包含docstrings的普通Python文件通常不会被扫描以查找fixture,除非通过python_files明确配置。
此外,当执行文本doctest文件时,支持使用usefixtures标记和标记为autouse的fixture。
doctest_namespace
fixture
doctest_namespace
fixture 可以用于将项目注入到你运行的 doctest 的命名空间中。它旨在在你的自定义 fixture 中使用,以便为使用这些 fixture 的测试提供上下文。这样,你就可以在 doctest 环境中预先定义一些变量或函数,使得 doctest 可以直接使用它们,而无需在文档字符串中重复声明。
doctest_namespace
是一个标准的字典对象,你可以在其中放置希望在 doctest 命名空间中出现的对象。这样,你就可以在 doctest 中直接使用这些对象,而无需在文档字符串中声明它们。
# conftest.py 文件内容
import pytest
import numpy
@pytest.fixture(autouse=True)
def add_np(doctest_namespace):
doctest_namespace["np"] = numpy
然后,在你的 doctest 中可以直接使用 np
:
# 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
"""
这样做会跳过第一个检查(即 random.random()
的调用及其预期输出),但不会跳过第二个检查(即 1 + 1
的结果)。# doctest: +SKIP
注释紧随在被跳过的检查之后,告诉 doctest 忽略这个特定的检查。这对于那些输出可能每次运行都不同(如随机数生成)或依赖于当前环境(如系统时间、文件存在性)的检查特别有用。
pytest 还允许在 doctest 内部使用标准的 pytest 函数 pytest.skip() 和 pytest.xfail(),这是因为你可以根据外部条件来跳过或预期失败(xfail)测试,这样做可能很有用:
>>> import sys, pytest
>>> if sys.platform.startswith('win'):
... pytest.skip('this doctest does not work on Windows')
...
>>> import fcntl
>>> ...
然而,使用这些函数是不被推荐的,因为它降低了文档字符串的可读性。将测试逻辑(如条件判断和跳过/预期失败)混合在文档字符串中会使文档难以理解和维护。此外,doctest 的主要目的是验证文档中的示例代码是否按预期工作,而不是作为全面的测试框架。
注意
pytest.skip() 和 pytest.xfail() 的行为取决于 doctest 是位于 Python 文件(在文档字符串中)还是位于包含与文本交织的 doctest 的文本文件中:
-
Python 模块(文档字符串):这些函数仅在该特定的文档字符串中起作用,允许同一模块中的其他文档字符串按正常方式执行。
-
文本文件:这些函数将跳过/预期失败(xfail)整个文件中剩余的所有检查。
替代方案
虽然内置的 pytest 支持为使用 doctest 提供了一套很好的功能,但如果你广泛使用 doctest,那么你可能会对那些提供更多特性并包含 pytest 集成的外部包感兴趣:
-
pytest-doctestplus:提供了高级的 doctest 支持,并允许测试 reStructuredText(“.rst”)文件。这个插件扩展了 pytest 的 doctest 功能,包括更好的错误报告、跳过测试的支持、以及更灵活的测试文件选择等。
-
Sybil:提供了一种通过从文档源中解析示例并在常规测试运行中评估这些解析后的示例来测试文档中示例的方法。Sybil 专注于从 Sphinx 文档等源中自动提取和执行示例代码,这对于确保文档中的代码示例保持最新和正确非常有用。
这些外部包为 doctest 提供了额外的灵活性和功能,使得在大型项目或需要高级测试特性的场景中更容易使用 doctest。选择哪个包取决于你的具体需求,比如你是否需要测试 reStructuredText 文件,或者你是否想从 Sphinx 文档中自动提取示例进行测试。