17.3. doctest — 通过文档来测试

目标:编写自动化测试作为模块文档的一部分。

doctest 通过运行嵌入在文档中的示例代码片段并验证是否产生预期的结果来测试源代码。它的工作原理是解析帮助文档以找到示例代码,运行之,然后将输出结果与预期值进行比较。许多开发者发现 doctest 相比 unittest 会更加容易使用  unittest。简单来说, 这是因为 doctest 在使用前不需要学习 API 。 然而,随着示例代码的愈发复杂及 Fixture 管理器的缺少,将会使编写 doctest 测试比使用 unittest 更麻烦。 unittest

开始

设置 doctests 的第一步是使用交互式会话创建示例,然后把示例复制粘贴到模块的文档字符串中。下面,,函数 my_function() 给出了两个例子:

doctest_simple.py

def my_function(a, b):
    """
    >>> my_function(2, 3)
    6
    >>> my_function('a', 3)
    'aaa'
    """
    return a * b

要运行测试用例,通过 -m 选项把 doctest 作为主程序。通常在测试运行时是不会产生输出结果的, 因此下面的示例包含了 -v 选项使输出结果更加详细。

$ python3 -m doctest -v doctest_simple.py

Trying:
    my_function(2, 3)
Expecting:
    6
ok
Trying:
    my_function('a', 3)
Expecting:
    'aaa'
ok
1 items had no tests:
    doctest_simple
1 items passed all tests:
   2 tests in doctest_simple.my_function
2 tests in 2 items.
2 passed and 0 failed.
Test passed.

示例代码通常无法单独作为函数的解释,因此 doctest 也允许包含其他文本。它寻找以解释器提示符 (>>>) 开头的行作为测试用例的开始,以空行或者下一个解释器提示符作为测试用例的结束。包含的解释文本会被忽略,只要它看起来不像测试用例,就可以有任何的格式。

doctest_simple_with_docs.py

def my_function(a, b):
    """Returns a * b.

    Works with numbers:

    >>> my_function(2, 3)
    6

    and strings:

    >>> my_function('a', 3)
    'aaa'
    """
    return a * b

更新的文档字符串中包含的文本使其对读者更友好。由于包含的文本会被 doctest 忽略,所以结果是一样的。

$ python3 -m doctest -v doctest_simple_with_docs.py

Trying:
    my_function(2, 3)
Expecting:
    6
ok
Trying:
    my_function('a', 3)
Expecting:
    'aaa'
ok
1 items had no tests:
    doctest_simple_with_docs
1 items passed all tests:
   2 tests in doctest_simple_with_docs.my_function
2 tests in 2 items.
2 passed and 0 failed.
Test passed.

处理不可预测的输出结果

在其他情况下,可能无法预测确切的结果,但仍应该是可测试的。例如,当地日期和时间值以及对象 ID 在每次测试运行时都会更改,浮点值表示中使用的默认精度取决于编译器选项,以及容器对象(如字典)的字符串表示是不确定的。虽然这些条件无法控制,但仍有处理这些的技术。

比如,在 CPython 中,对象标识符是基于保存这些对象的数据结构的内存地址。

doctest_unpredictable.py

class MyClass:
    pass

def unpredictable(obj):
    """Returns a new list containing obj.

    >>> unpredictable(MyClass())
    [<doctest_unpredictable.MyClass object at 0x10055a2d0>]
    """
    return [obj]

每次运行时,这些 ID 值都会改变,是因为它被加载到内存的不同地方。

$ python3 -m doctest -v doctest_unpredictable.py

Trying:
    unpredictable(MyClass())
Expecting:
    [<doctest_unpredictable.MyClass object at 0x10055a2d0>]
****************************************************************
File ".../doctest_unpredictable.py", line 17, in doctest_unpredi
ctable.unpredictable
Failed example:
    unpredictable(MyClass())
Expected:
    [<doctest_unpredictable.MyClass object at 0x10055a2d0>]
Got:
    [<doctest_unpredictable.MyClass object at 0x1047a2710>]
2 items had no tests:
    doctest_unpredictable
    doctest_unpredictable.MyClass
****************************************************************
1 items had failures:
   1 of   1 in doctest_unpredictable.unpredictable
1 tests in 3 items.
0 passed and 1 failed.
***Test Failed*** 1 failures.

在当测试代码包含那些可能以无法预测的方式改变的值,而且确切值对测试结果来说不重要的情况下,使用 ELLIPSIS 选项来告诉 doctest 忽略部分验证值。

doctest_ellipsis.py

class MyClass:
    pass

def unpredictable(obj):
    """Returns a new list containing obj.

    >>> unpredictable(MyClass()) #doctest: +ELLIPSIS
    [<doctest_ellipsis.MyClass object at 0x...>]
    """
    return [obj]

在调用函数 unpredictable() 之后的注释 #doctest: +ELLIPSIS 告诉 doctest 打开该测试的 ELLIPSIS 选项。... 则替换对象 ID 中的内存地址,因此忽略了期待值中的相应部分,输出结果相匹配从而通过测试。

$ python3 -m doctest -v doctest_ellipsis.py

Trying:
    unpredictable(MyClass()) #doctest: +ELLIPSIS
Expecting:
    [<doctest_ellipsis.MyClass object at 0x...>]
ok
2 items had no tests:
    doctest_ellipsis
    doctest_ellipsis.MyClass
1 items passed all tests:
   1 tests in doctest_ellipsis.unpredictable
1 tests in 3 items.
1 passed and 0 failed.
Test passed.

在某些情况下,不确定的值也无法被忽略,这是因为忽略这些值可能会使测试不完整或者不准确。例如,当处理字符串表示不一致的数据类型时,简单的测试会变得愈发复杂。就比如说,字典的字符串形式会根据键的添加顺序而改变。

doctest_hashed_values.py

keys = ['a', 'aa', 'aaa']

print('dict:', {k: len(k) for k in keys})
print('set :', set(keys))

由于哈希值的随机化和密钥冲突,每次脚本运行时,字典的内部密钥列表顺序可能会不同。集合使用相同的哈希算法,并且表现出相同的行为。

$ python3 doctest_hashed_values.py

dict: {'aa': 2, 'a': 1, 'aaa': 3}
set : {'aa', 'a', 'aaa'}

$ python3 doctest_hashed_values.py

dict: {'a': 1, 'aa': 2, 'aaa': 3}
set : {'a', 'aa', 'aaa'}

处理这些潜在差异的最好方法是创建能够产生不太容易改变的值的测试代码。在字典和集合的情况下,可能意味着通过单独地寻找特定的键,生成数据结构内容的排序列表,或者和文字值进行比较来获得相等的值而不是通过字符串表示。

doctest_hashed_values_tests.py

import collections

def group_by_length(words):
    """Returns a dictionary grouping words into sets by length.

    >>> grouped = group_by_length([ 'python', 'module', 'of',
    ... 'the', 'week' ])
    >>> grouped == { 2:set(['of']),
    ...              3:set(['the']),
    ...              4:set(['week']),
    ...              6:set(['python', 'module']),
    ...              }
    True

    """
    d = collections.defaultdict(set)
    for word in words:
        d[len(word)].add(word)
    return d

上述单个示例代码实际上作为两个单独的测试,第一个期望没有控制台输出结果,而第二个期望输出比较操作的布尔结果。

$ python3 -m doctest -v doctest_hashed_values_tests.py

Trying:
    grouped = group_by_length([ 'python', 'module', 'of',
    'the', 'week' ])
Expecting nothing
ok
Trying:
    grouped == { 2:set(['of']),
                 3:set(['the']),
                 4:set(['week']),
                 6:set(['python', 'module']),
                 }
Expecting:
    True
ok
1 items had no tests:
    doctest_hashed_values_tests
1 items passed all tests:
   2 tests in doctest_hashed_values_tests.group_by_length
2 tests in 2 items.
2 passed and 0 failed.
Test passed.

Tracebacks

Traceback 是变化数据的一个特例。由于 traceback 中的路径取决于模块安装在文件系统的位置,因此如果像其他输出一样处理的话,无法写可移植测试。

doctest_tracebacks.py

def this_raises():
    """This function always raises an exception.

    >>> this_raises()
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
      File "/no/such/path/doctest_tracebacks.py", line 14, in
      this_raises
        raise RuntimeError('here is the error')
    RuntimeError: here is the error
    """
    raise RuntimeError('here is the error')

doctest 为 traceback 做了特殊处理,忽略随系统变化的部份。

$ python3 -m doctest -v doctest_tracebacks.py

Trying:
    this_raises()
Expecting:
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
      File "/no/such/path/doctest_tracebacks.py", line 14, in
      this_raises
        raise RuntimeError('here is the error')
    RuntimeError: here is the error
ok
1 items had no tests:
    doctest_tracebacks
1 items passed all tests:
   1 tests in doctest_tracebacks.this_raises
1 tests in 2 items.
1 passed and 0 failed.
Test passed.

实际上,整个 traceback 主体被忽略,可以不用写。

doctest_tracebacks_no_body.py

def this_raises():
    """This function always raises an exception.

    >>> this_raises()
    Traceback (most recent call last):
    RuntimeError: here is the error

    >>> this_raises()
    Traceback (innermost last):
    RuntimeError: here is the error
    """
    raise RuntimeError('here is the error')

doctest 碰到 traceback 标题行时(不管「Traceback (most recent call last) 」或 「Traceback (innermost last):」),为了支持 Python 的不同版本,它向前跳过主体,去匹配异常的类型和消息,完全忽略那些干扰行。

$ python3 -m doctest -v doctest_tracebacks_no_body.py

Trying:
    this_raises()
Expecting:
    Traceback (most recent call last):
    RuntimeError: here is the error
ok
Trying:
    this_raises()
Expecting:
    Traceback (innermost last):
    RuntimeError: here is the error
ok
1 items had no tests:
    doctest_tracebacks_no_body
1 items passed all tests:
   2 tests in doctest_tracebacks_no_body.this_raises
2 tests in 2 items.
2 passed and 0 failed.
Test passed.

处理空白

在实际应用中,为了让可读性更好,输出通常有空白,比如空行、制表符和额外的空格。特别是空行会给 doctest 带来问题,因为空行用来分隔测试。

doctest_blankline_fail.py

def double_space(lines):
    """Prints a list of lines double-spaced.

    >>> double_space(['Line one.', 'Line two.'])
    Line one.

    Line two.

    """
    for l in lines:
        print(l)
        print()

double_space() 接受一个输入行列表,打印每个输入行时,后面多打印一个空行。

$ python3 -m doctest -v doctest_blankline_fail.py

Trying:
    double_space(['Line one.', 'Line two.'])
Expecting:
    Line one.
****************************************************************
File ".../doctest_blankline_fail.py", line 12, in doctest_blankl
ine_fail.double_space
Failed example:
    double_space(['Line one.', 'Line two.'])
Expected:
    Line one.
Got:
    Line one.
    <BLANKLINE>
    Line two.
    <BLANKLINE>
1 items had no tests:
    doctest_blankline_fail
****************************************************************
1 items had failures:
   1 of   1 in doctest_blankline_fail.double_space
1 tests in 2 items.
0 passed and 1 failed.
***Test Failed*** 1 failures.

这个测试失败是因为 Line one 后面的空行被解释为分隔行,即示例输出结束了。为了匹配空行,要将它们替换为 <BLANKLINE>

doctest_blankline.py

def double_space(lines):
    """Prints a list of lines double-spaced.

    >>> double_space(['Line one.', 'Line two.'])
    Line one.
    <BLANKLINE>
    Line two.
    <BLANKLINE>
    """
    for l in lines:
        print(l)
        print()

doctest 在执行比较之前,将实际的空行替换为相同的字符常量,因此实际值与期望值匹配,测试通过。

$ python3 -m doctest -v doctest_blankline.py

Trying:
    double_space(['Line one.', 'Line two.'])
Expecting:
    Line one.
    <BLANKLINE>
    Line two.
    <BLANKLINE>
ok
1 items had no tests:
    doctest_blankline
1 items passed all tests:
   1 tests in doctest_blankline.double_space
1 tests in 2 items.
1 passed and 0 failed.
Test passed.

行内空白在测试中也会带来棘手问题。下面这个例子在 6 后多一个空格。

doctest_extra_space.py

def my_function(a, b):
    """
    >>> my_function(2, 3)
    6
    >>> my_function('a', 3)
    'aaa'
    """
    return a * b

复制粘贴代码时会在行尾引入多余的空格,由于在行尾,在源文件中注意不到,测试失败报告时也看不见。

$ python3 -m doctest -v doctest_extra_space.py

Trying:
    my_function(2, 3)
Expecting:
    6
****************************************************************
File ".../doctest_extra_space.py", line 15, in doctest_extra_spa
ce.my_function
Failed example:
    my_function(2, 3)
Expected:
    6
Got:
    6
Trying:
    my_function('a', 3)
Expecting:
    'aaa'
ok
1 items had no tests:
    doctest_extra_space
****************************************************************
1 items had failures:
   1 of   2 in doctest_extra_space.my_function
2 tests in 2 items.
1 passed and 1 failed.
***Test Failed*** 1 failures.

基于 diff 报告方式的其中一种,例如 REPORT_NDIFF ,使用它可显示实际值与期望值之间差异的更多细节,可看见多余的空白。

doctest_ndiff.py

def my_function(a, b):
    """
    >>> my_function(2, 3) #doctest: +REPORT_NDIFF
    6
    >>> my_function('a', 3)
    'aaa'
    """
    return a * b

REPORT_UDIFFREPORT_CDIFF这两种选项也可以用,它们输出的可读性更好。

$ python3 -m doctest -v doctest_ndiff.py

Trying:
    my_function(2, 3) #doctest: +REPORT_NDIFF
Expecting:
    6
****************************************************************
File ".../doctest_ndiff.py", line 16, in doctest_ndiff.my_functi
on
Failed example:
    my_function(2, 3) #doctest: +REPORT_NDIFF
Differences (ndiff with -expected +actual):
    - 6
    ?  -
    + 6
Trying:
    my_function('a', 3)
Expecting:
    'aaa'
ok
1 items had no tests:
    doctest_ndiff
****************************************************************
1 items had failures:
   1 of   2 in doctest_ndiff.my_function
2 tests in 2 items.
1 passed and 1 failed.
***Test Failed*** 1 failures.

在有些情况下,在测试示例的输出中增加额外的空白并让 doctest 忽略是有好处的。比如,尽管可写成一行,但写成多行时数据结构更容易看清楚。


def my_function(a, b):
    """Returns a * b.

    >>> my_function(['A', 'B'], 3) #doctest: +NORMALIZE_WHITESPACE
    ['A', 'B',
     'A', 'B',
     'A', 'B']

    This does not match because of the extra space after the [ in
    the list.

    >>> my_function(['A', 'B'], 2) #doctest: +NORMALIZE_WHITESPACE
    [ 'A', 'B',
      'A', 'B', ]
    """
    return a * b

NORMALIZE_WHITESPACE 打开时,实际值和期望值中的任意长度空格都被视为匹配。没有空白和有空白无法匹配,但长度可以不一致。 第一个测试例子符合规则并通过,即使有额外的空格和换行符。 第二个例子在 [ 后面和 ] 前面有额外的空格,因此失败。

$ python3 -m doctest -v doctest_normalize_whitespace.py

Trying:
    my_function(['A', 'B'], 3) #doctest: +NORMALIZE_WHITESPACE
Expecting:
    ['A', 'B',
     'A', 'B',
     'A', 'B',]
***************************************************************
File "doctest_normalize_whitespace.py", line 13, in doctest_nor
malize_whitespace.my_function
Failed example:
    my_function(['A', 'B'], 3) #doctest: +NORMALIZE_WHITESPACE
Expected:
    ['A', 'B',
     'A', 'B',
     'A', 'B',]
Got:
    ['A', 'B', 'A', 'B', 'A', 'B']
Trying:
    my_function(['A', 'B'], 2) #doctest: +NORMALIZE_WHITESPACE
Expecting:
    [ 'A', 'B',
      'A', 'B', ]
***************************************************************
File "doctest_normalize_whitespace.py", line 21, in doctest_nor
malize_whitespace.my_function
Failed example:
    my_function(['A', 'B'], 2) #doctest: +NORMALIZE_WHITESPACE
Expected:
    [ 'A', 'B',
      'A', 'B', ]
Got:
    ['A', 'B', 'A', 'B']
1 items had no tests:
    doctest_normalize_whitespace
***************************************************************
1 items had failures:
   2 of   2 in doctest_normalize_whitespace.my_function
2 tests in 2 items.
0 passed and 2 failed.
***Test Failed*** 2 failures.

测试的位置

目前为止例子中的所有测试都写在被测函数的 docstring 中。对习惯使用函数时查看 docstring 的用户来说,这很方便(尤其和 pydoc 一起使用),但 doctest 也去其他地方找测试。很明显,其他测试也会出现在模块内的 docstring 中。

doctest_docstrings.py

"""Tests can appear in any docstring within the module.

Module-level tests cross class and function boundaries.

>>> A('a') == B('b')
False
"""

class A:
    """Simple class.

    >>> A('instance_name').name
    'instance_name'
    """

    def __init__(self, name):
        self.name = name

    def method(self):
        """Returns an unusual value.

        >>> A('name').method()
        'eman'
        """
        return ''.join(reversed(self.name))

class B(A):
    """Another simple class.

    >>> B('different_name').name
    'different_name'
    """

模块、类和函数层级的 docstring 都能包含测试。

$ python3 -m doctest -v doctest_docstrings.py

Trying:
    A('a') == B('b')
Expecting:
    False
ok
Trying:
    A('instance_name').name
Expecting:
    'instance_name'
ok
Trying:
    A('name').method()
Expecting:
    'eman'
ok
Trying:
    B('different_name').name
Expecting:
    'different_name'
ok
1 items had no tests:
    doctest_docstrings.A.__init__
4 items passed all tests:
   1 tests in doctest_docstrings
   1 tests in doctest_docstrings.A
   1 tests in doctest_docstrings.A.method
   1 tests in doctest_docstrings.B
4 tests in 5 items.
4 passed and 0 failed.
Test passed.

有些情况下我们需要测试在模块的源码中,但不要出现在模块的帮助文本中,因此需要一个 docstring 之外的其他位置。doctest 会查看模块级别的 __test__ 变量,利用这里的信息去定位其他测试。__test__ 应是一个字典,它将测试集名称(字符串)映射到字符串、模块、类、或函数。

doctest_private_tests.py

import doctest_private_tests_external

__test__ = {
    'numbers': """
>>> my_function(2, 3)
6

>>> my_function(2.0, 3)
6.0
""",

    'strings': """
>>> my_function('a', 3)
'aaa'

>>> my_function(3, 'a')
'aaa'
""",

    'external': doctest_private_tests_external,
}

def my_function(a, b):
    """Returns a * b
    """
    return a * b

如果某个键的值是字符串,它被看作是一个 docstring ,并直接从中扫描测试内容。如果值是一个类或函数,doctest 从它们的 docstring 中扫描。在本例中,模块 doctest_private_tests_external 的 docstring 中有一个测试。

doctest_private_tests_external.py

"""External tests associated with doctest_private_tests.py.

>>> my_function(['A', 'B', 'C'], 2)
['A', 'B', 'C', 'A', 'B', 'C']
"""

扫描该示例文件后,doctest 总共找到5个测试。

$ python3 -m doctest -v doctest_private_tests.py

Trying:
    my_function(['A', 'B', 'C'], 2)
Expecting:
    ['A', 'B', 'C', 'A', 'B', 'C']
ok
Trying:
    my_function(2, 3)
Expecting:
    6
ok
Trying:
    my_function(2.0, 3)
Expecting:
    6.0
ok
Trying:
    my_function('a', 3)
Expecting:
    'aaa'
ok
Trying:
    my_function(3, 'a')
Expecting:
    'aaa'
ok
2 items had no tests:
    doctest_private_tests
    doctest_private_tests.my_function
3 items passed all tests:
   1 tests in doctest_private_tests.__test__.external
   2 tests in doctest_private_tests.__test__.numbers
   2 tests in doctest_private_tests.__test__.strings
5 tests in 5 items.
5 passed and 0 failed.
Test passed.

外部文档

将测试混合在代码中不是使用 doctest 的唯一方式。也能用外部的项目文档,比如 reStructuredText 文件。

doctest_in_help.py

def my_function(a, b):
    """Returns a*b
    """
    return a * b

本示例模块的帮助内容保存到另外一个单独文件中,doctest_in_help.txt 。本例演示如何在帮助文本中包含这些模块,接着让 doctest 找到并运行。

doctest_in_help.txt

===============================
 How to Use doctest_in_help.py
===============================

This library is very simple, since it only has one function called
``my_function()``.

Numbers
=======

``my_function()`` returns the product of its arguments.  For numbers,
that value is equivalent to using the ``*`` operator.

::

    >>> from doctest_in_help import my_function
    >>> my_function(2, 3)
    6

It also works with floating-point values.

::

    >>> my_function(2.0, 3)
    6.0

Non-Numbers
===========

Because ``*`` is also defined on data types other than numbers,
``my_function()`` works just as well if one of the arguments is a
string, a list, or a tuple.

::

    >>> my_function('a', 3)
    'aaa'

    >>> my_function(['A', 'B', 'C'], 2)
    ['A', 'B', 'C', 'A', 'B', 'C']

文本文件中的测试能从命令行运行,像 Python 模块一样。

$ python3 -m doctest -v doctest_in_help.txt

Trying:
    from doctest_in_help import my_function
Expecting nothing
ok
Trying:
    my_function(2, 3)
Expecting:
    6
ok
Trying:
    my_function(2.0, 3)
Expecting:
    6.0
ok
Trying:
    my_function('a', 3)
Expecting:
    'aaa'
ok
Trying:
    my_function(['A', 'B', 'C'], 2)
Expecting:
    ['A', 'B', 'C', 'A', 'B', 'C']
ok
1 items passed all tests:
   5 tests in doctest_in_help.txt
5 tests in 1 items.
5 passed and 0 failed.
Test passed.

正常情况下,doctest 建立测试运行环境,包括被测模块成员,因此测试不需显式 import 模块。然而,本例中的测试并不在模块内定义,doctest 并不知道如何建立全局命名空间,因此这个例子需要自己完成 import 的工作。在一个给定文件中,所有测试共享同一个运行环境,所以在开头只做一次 import 就足够。

运行测试

前面的例子全都使用 doctest 内置的命令行测试运行器。对一个单独模块来说,这方便快捷,但随着软件包扩充到多个文件,很快变得重复枯燥。有几种替代方法。

通过模块

以源码运行 doctest 的说明可放在模块的底部。

doctest_testmod.py

def my_function(a, b):
    """
    >>> my_function(2, 3)
    6
    >>> my_function('a', 3)
    'aaa'
    """
    return a * b

if __name__ == '__main__':
    import doctest
    doctest.testmod()

只在当前模块为 __main__ 时调用 testmod() ,这保证只有在模块作为主程序时才运行测试。

$ python3 doctest_testmod.py -v

Trying:
    my_function(2, 3)
Expecting:
    6
ok
Trying:
    my_function('a', 3)
Expecting:
    'aaa'
ok
1 items had no tests:
    __main__
1 items passed all tests:
   2 tests in __main__.my_function
2 tests in 2 items.
2 passed and 0 failed.
Test passed.

testmod() 的第一个参数是一个模块,它将被扫描获取测试内容。一个单独的测试脚本可以使用这个特性去 import 实际代码,一个接一个地运行每个模块内的测试。

doctest_testmod_other_module.py

import doctest_simple

if __name__ == '__main__':
    import doctest
    doctest.testmod(doctest_simple)

可以导入每个模块并运行它们的测试形成一个项目的测试集。

$ python3 doctest_testmod_other_module.py -v

Trying:
    my_function(2, 3)
Expecting:
    6
ok
Trying:
    my_function('a', 3)
Expecting:
    'aaa'
ok
1 items had no tests:
    doctest_simple
1 items passed all tests:
   2 tests in doctest_simple.my_function
2 tests in 2 items.
2 passed and 0 failed.
Test passed.

通过文件

testfile() 类似 testmod() ,允许测试程序从外部文件显式执行测试。

doctest_testfile.py

import doctest

if __name__ == '__main__':
    doctest.testfile('doctest_in_help.txt')

testmod()testfile() 都有可选的参数通过 doctest 选项来控制测试的行为。这些功能的细节请参阅标准库文档 -- 大多数情况下不需要它们。

$ python3 doctest_testfile.py -v

Trying:
    from doctest_in_help import my_function
Expecting nothing
ok
Trying:
    my_function(2, 3)
Expecting:
    6
ok
Trying:
    my_function(2.0, 3)
Expecting:
    6.0
ok
Trying:
    my_function('a', 3)
Expecting:
    'aaa'
ok
Trying:
    my_function(['A', 'B', 'C'], 2)
Expecting:
    ['A', 'B', 'C', 'A', 'B', 'C']
ok
1 items passed all tests:
   5 tests in doctest_in_help.txt
5 tests in 1 items.
5 passed and 0 failed.
Test passed.

Unittest 测试集

当用 unittestdoctest 测试不同情况下的同一段代码时,可让 unittest 集成 doctest 一起来运行测试。DocTestSuiteDocFileSuite 这两个类可以创建与 unittest API 兼容的测试集。

doctest_unittest.py

import doctest
import unittest

import doctest_simple

suite = unittest.TestSuite()
suite.addTest(doctest.DocTestSuite(doctest_simple))
suite.addTest(doctest.DocFileSuite('doctest_in_help.txt'))

runner = unittest.TextTestRunner(verbosity=2)
runner.run(suite)

每个来源的测试都会折叠成一个结果,而不是报告每个测试。

$ python3 doctest_unittest.py

my_function (doctest_simple)
Doctest: doctest_simple.my_function ... ok
doctest_in_help.txt
Doctest: doctest_in_help.txt ... ok

----------------------------------------------------------------
Ran 2 tests in 0.003s

OK

测试上下文

doctest 运行测试时创建的运行上下文包含被测模块在模块层级的全局变量副本。每个测试源(函数、类、模块)都有自己的全局变量集,一定程度上相互隔离测试,因此它们很少相互干扰。

doctest_test_globals.py

class TestGlobals:

    def one(self):
        """
        >>> var = 'value'
        >>> 'var' in globals()
        True
        """

    def two(self):
        """
        >>> 'var' in globals()
        False
        """

TestGlobals 有两个成员方法:one()two()one() docstring 中的测试给一个全局变量赋值,two() 去查找该全局变量(并期望找不到它)。

$ python3 -m doctest -v doctest_test_globals.py

Trying:
    var = 'value'
Expecting nothing
ok
Trying:
    'var' in globals()
Expecting:
    True
ok
Trying:
    'var' in globals()
Expecting:
    False
ok
2 items had no tests:
    doctest_test_globals
    doctest_test_globals.TestGlobals
2 items passed all tests:
   2 tests in doctest_test_globals.TestGlobals.one
   1 tests in doctest_test_globals.TestGlobals.two
3 tests in 4 items.
3 passed and 0 failed.
Test passed.

然而这并不是说,如果改变了模块中定义的可变变量,测试也 不能 相互干扰,

doctest_mutable_globals.py

_module_data = {}

class TestGlobals:

    def one(self):
        """
        >>> TestGlobals().one()
        >>> 'var' in _module_data
        True
        """
        _module_data['var'] = 'value'

    def two(self):
        """
        >>> 'var' in _module_data
        False
        """

模块变量 _module_dataone() 的测试改变,导致 two() 的测试失败。

$ python3 -m doctest -v doctest_mutable_globals.py

Trying:
    TestGlobals().one()
Expecting nothing
ok
Trying:
    'var' in _module_data
Expecting:
    True
ok
Trying:
    'var' in _module_data
Expecting:
    False
****************************************************************
File ".../doctest_mutable_globals.py", line 25, in doctest_mutab
le_globals.TestGlobals.two
Failed example:
    'var' in _module_data
Expected:
    False
Got:
    True
2 items had no tests:
    doctest_mutable_globals
    doctest_mutable_globals.TestGlobals
1 items passed all tests:
   2 tests in doctest_mutable_globals.TestGlobals.one
****************************************************************
1 items had failures:
   1 of   1 in doctest_mutable_globals.TestGlobals.two
3 tests in 4 items.
2 passed and 1 failed.
***Test Failed*** 1 failures.

如果为了参数化环境而需为测试设置全局值,可以将值传递给 testmode()testfile() ,以便调用者利用数据建立上下文。

See also

  • doctest标准库文档
  • 强大的字典 -- Brandon Rhodes在 PyCon 2010 上的有关 dict 内部操作的主题演讲。
  • difflib -- Python 序列差异计算库,用来生成 ndiff 输出。
  • Sphinx -- 除了作为 Python 标准库的文档处理工具之外,Sphinx 还被许多第三方项目采用,因为它易于使用并以多种数字和打印格式生成干净的输出。Sphinx 包含一个用于运行 doctest 的扩展,与处理文档源文件一样,因此示例总是准确的。
  • py.test -- 支持 doctest 的第三方测试运行器。
  • nose2 -- 支持 doctest 的第三方测试运行器。
  • Manuel -- 第三方基于文档的测试运行器,具有更高级的测试用例提取和Sphinx集成。