目录

doctest——实用但容易被忽略的模块

前言

本文将简单地介绍一下 Python 的 doctest 是什么、如何用它测试 docstring 中的代码、以及它是如何工作的等内容。

注意

本文中的命令行示范,都是基于以下环境:

  • 操作系统: CentOS 7
  • Python: 3.7.9

 

什么是 doctest

doctest是 Python 引入的一个标准模块,用于在代码的 docstring 中搜索像 Python 交互式会话一样的语句,然后去执行它,并把执行结果和指定的结果做比较,从而检验这些交互式语句能像期望一样的工作。

这样描述可能仍然会让你一头雾水。本人尝试”中翻中“吧:doctest 就是可以让你在 docstring 里放 Python 命令并去执行它,然后检验执行结果是否和期望一样。

这里先举一个简单的例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
"""
This file provide an example about doctest

>>> reverse_data('abcdefg')
'gfedcba'
"""

def reverse_data(origin_data):
    """Return the reverse of original data.

    >>> reverse_data('hello world')
    'dlrow olleh'
    >>> reverse_data([1, 3, 5, 7, 9])
    [9, 7, 5, 3, 1]
    >>> reverse_data((1, 2, 3, 4, 5))
    (5, 4, 3, 2, 1)
    >>> reverse_data({1: "a", 2: "b", 3: "c"})
    Traceback (most recent call last):
      ...
    TypeError: unhashable type: 'slice'
    >>> reverse_data({1, 3, 5})
    Traceback (most recent call last):
      ...
    TypeError: 'set' object is not subscriptable
    """
    return origin_data[::-1]


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

把以上代码保存到 test_doctest.py,并在命令行终端运行:

1
$ python test_doctest.py -v

结果是:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
Trying:
    reverse_data('abcdefg')
Expecting:
    'gfedcba'
ok
Trying:
    reverse_data('hello world')
Expecting:
    'dlrow olleh'
ok
Trying:
    reverse_data([1, 3, 5, 7, 9])
Expecting:
    [9, 7, 5, 3, 1]
ok
Trying:
    reverse_data((1, 2, 3, 4, 5))
Expecting:
    (5, 4, 3, 2, 1)
ok
Trying:
    reverse_data({1: "a", 2: "b", 3: "c"})
Expecting:
    Traceback (most recent call last):
      ...
    TypeError: unhashable type: 'slice'
ok
Trying:
    reverse_data({1, 3, 5})
Expecting:
    Traceback (most recent call last):
      ...
    TypeError: 'set' object is not subscriptable
ok
2 items passed all tests:
   1 tests in __main__
   5 tests in __main__.reverse_data
6 tests in 2 items.
6 passed and 0 failed.
Test passed.
注意
运行上述代码时,要加 -v 选项,否则不会打印输出到终端。

可以看到,Python 在执行 doctest 的testmod()函数时,会尝试找出该文件中的所有 docstring 中的以>>>...开头的语句并尝试执行,这些语句称为交互式语句,然后把执行结果和交互式语句后面一行的语句作比较,后面一行的语句表示期望结果。

哪些场合会用 doctest 呢?主要有以下几种情况:

  • 可以通过验证 docstring 中的交互式语句是否像描述的那样执行,从而检查 docstring 是否是最新的;
  • 可以通过验证测试文件或测试对象中的交互式语句是否能够像期望一样运行,来进行回归测试;
  • 可以为一个包或模块写教程,教程中有丰富的交互式语句来展示输入/输出,然后用 doctest 测试该教程文档。这类文档也称为 ”可执行文档“。

 

使用方式

有两种方式来使用 doctest:执行 docstring 中的交互式语句,或者执行文本文件中的交互式语句。

执行 docstring 中的交互式语句

上一节的例子已经已经展示如何做。要检查一个 Python 脚本中的交互式语句,最简单的方式就是在脚本文件的结尾加上:

1
2
3
if __name__ == "__main__":
    import doctest
    doctest.testmod()

然后执行该脚本,别忘了带上-v参数:

1
python -v test_doctest.py

你也可以把verbose=True传递给testmod()函数,从而无论命令行有没有带-v参数,都能显示 doctest 结果。

另一种执行 docstring 中交互式语句的方式是通过命令行参数-m doctest让 Python 解释器直接导入 doctest 并执行脚本中的交互式语句:

1
python -m doctest -v test_doctest.py

用这种方式,不需要在 Python 脚本的末尾添加前面的三行代码,所以这种方式更通用。

执行文本文件中的交互式语句

另一种用 doctest 的方式是执行文本文件中的交互式语句。首先,在文本文件example.txt中添加你要执行并测试的语句:

1
2
3
4
5
6
7
8
execute command in this ``example.txt`` file
======================

Call ``reverse_data``

>>> from test_doctest import reverse_data
>>> reverse_data('hello world')
'dlraw olleh'

然后在命令行启动 Python,执行以下语句:

1
2
import doctest
doctest.testfile("example.txt")

或者,把上面两行代码保存在一个 Python 脚本中并执行脚本。两种方式的执行结果相同:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
**********************************************************************
File "./example.txt", line 7, in example.txt
Failed example:
    reverse_data('hello world')
Expected:
    'dlraw olleh'
Got:
    'dlrow olleh'
**********************************************************************
1 items had failures:
   1 of   2 in example.txt
***Test Failed*** 1 failures.

testmod()略有不同,当测试成功,没有错误的时候,testfile()不会打印出具体过程。和testmod()相同的是,我们也可以通过-m doctest参数在命令行运行:

1
python -m doctest -v example.txt

执行的结果和上面相同。

这种方式最大的优点是:把测试对象和测试语句分开。测试对象是一个 Python 脚本,而测试语句单独定义在另一个文本文件中。这样,对于任何一个 Python 脚本,你都可以在不改变它的内容的前提下,另外编写一个测试文本文件,用来测试该脚本实现的功能。

 

doctest 如何工作

检测哪些对象的 docstring

我们知道,docstring 可以定义在不同的地方:可以是脚本文件的全局处,可以是类中,也可以是函数中。另外,脚本导入的其它模块中,也可以有 docstring。那么,在对一个特定的 Python 脚本执行 doctest 时候,它会检测哪些 docstring 呢?

doctest 只会检测该脚本的模块 docstring(定义在脚本文件全局处)、函数 docstring、类 docstring 以及类的方法的 docstring,而不会检测该脚本导入的那些模块中的 docstring。

除此之外,doctest 还会检测该脚本文件的__test__变量,我们可以通过设置脚本的__test__变量,来添加我们想要检测的对象。该变量是个字典类型,我们可以在该字典中添加自定义的函数对象、类对象、字符串。字典的键是标识这些对象的名称,值是这些具体的对象。当值是函数对象、类对象的时候,doctest 会去检测这些对象中的 docstring,当值是字符串时,doctest 会把它当做 docstring。

比如,在 什么是 doctest 一节的例子中,添加一个字符串 literal_docstring,包含交互式语句,并把它加入到__test__变量中:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
"""
This file provide an example about doctest

>>> reverse_data('abcdefg')
'gfedcba'
"""

def reverse_data(origin_data):
    """Return the reverse of original data.

    >>> reverse_data('hello world')
    'dlrow olleh'
    >>> reverse_data([1, 3, 5, 7, 9])
    [9, 7, 5, 3, 1]
    >>> reverse_data((1, 2, 3, 4, 5))
    (5, 4, 3, 2, 1)
    >>> reverse_data({1: "a", 2: "b", 3: "c"})
    Traceback (most recent call last):
      ...
    TypeError: unhashable type: 'slice'
    >>> reverse_data({1, 3, 5})
    Traceback (most recent call last):
      ...
    TypeError: 'set' object is not subscriptable
    """
    return origin_data[::-1]

literal_docstring = """
>>> reverse_data('12345')
'54321'
"""

__test__ = {'a literal docstring': literal_docstring}

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

执行结果相较于之前,会增加一个测试结果:

1
2
3
4
5
6
7
8
...
3 items passed all tests:
   1 tests in test_doctest
   1 tests in test_doctest.__test__.a literal docstring
   5 tests in test_doctest.reverse_data
7 tests in 3 items.
7 passed and 0 failed.
Test passed.

如何识别交互式语句及期望结果

前面提到过,交互式语句必须以>>>...开头,期望的结果必须紧跟在交互式语句后面,以空行或者下一个交互式语句标识结束。有些特殊情况需要考虑:

  • 期望结果中不能包含全是空格的一行。因为空行或者都是空格的一行,会被识别为期望结果的结束。如果需要表示期望结果是空行,可以用<BLANKLINE>标识。

  • 期望结果中所有的tab字符会被展开为多个空格,而交互式语句实际执行结果中的tab字符不会被展开。这就意味着如果实际的执行结果中包含tab字符,只有当设置doctest.NORMALIZE_WHITESPACE选项时,测试会通过。

  • 交互式语句执行后,只有它的标准输出(stdout)会被捕获,而错误输出(stderr)会被忽略。换句话说,和期望结果比较的是标准输出,而不是错误输出。

  • 如果在交互式语句中需要用反斜杠\表示换行,或者任何其它原因需要表示反斜杠,那么需要使用原始(raw) docstring,否则反斜杠会被识别为转义字符,比如:

    1
    2
    3
    4
    
    >>> def f(x):
    ...     r'''Backslashes in a raw docstring: m\n'''
    >>> print(f.__doc__)
    Backslashes in a raw docstring: m\n
    

doctest 执行时的上下文

默认情况下,每次 doctest 在找 docstring 准备测试的时候,它会对该模块中的所有全局对象做一份浅拷贝。所以,当执行测试的时候,不会改变这些全局对象原来的内容。这也意味着,交互式语句中可以自由地使用该模块中的全局对象,以及可以自由地使用交互式语句所在的 docstring 的之前定义的变量,但不能使用定义在其它 docstring 中的变量。 比如,把 什么是 doctest 一节的那个例子稍作修改:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
"""
This file provide an example about doctest

>>> str3 = 'test3'
>>> reverse_data(str3)
'3tset'
"""

str1 = 'test1'

def reverse_data(origin_data):
    """Return the reverse of original string.
    >>> str2 = 'test2'
    >>> reverse_data(str1)
    '1tset'
    >>> reverse_data(str2)
    '2tset'
    >>> reverse_data(str3)
    '3tset'
    """
    return origin_data[::-1]

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

运行结果:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
Trying:
    str3 = 'test3'
Expecting nothing
ok
Trying:
    reverse_data(str3)
Expecting:
    '3tset'
ok
Trying:
    str2 = 'test2'
Expecting nothing
ok
Trying:
    reverse_data(str1)
Expecting:
    '1tset'
ok
Trying:
    reverse_data(str2)
Expecting:
    '2tset'
ok
Trying:
    reverse_data(str3)
Expecting:
    '3tset'
**********************************************************************
File "/home/yinjunz/test_python/test_doctest.py", line 18, in test_doctest.reverse_data
Failed example:
    reverse_data(str3)
Exception raised:
    Traceback (most recent call last):
      File "/usr/local/python3.7.9/lib/python3.7/doctest.py", line 1337, in __run
        compileflags, 1), test.globs)
      File "<doctest test_doctest.reverse_data[3]>", line 1, in <module>
        reverse_data(str3)
    NameError: name 'str3' is not defined
1 items passed all tests:
   2 tests in test_doctest
**********************************************************************
1 items had failures:
   1 of   4 in test_doctest.reverse_data
6 tests in 2 items.
5 passed and 1 failed.
***Test Failed*** 1 failures.

可以看到,在函数reverse_data的 docstring 中,交互式语句可以访问全局对象 str1,也可以访问定义在该 docstring 中的对象 str2,但无法访问定义在全局处的 docstring 中的对象 str3。

如何定义期望的异常

可以期望交互式语句的执行结果是异常吗?当然可以,在 什么是 doctest 一节的例子中,已经展示了如何做。traceback 的完整信息类似下面:

1
2
3
4
5
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/yinjunz/test_python/test_doctest.py", line 21, in reverse_data
    return origin_data[::-1]
TypeError: unhashable type: 'slice'

其中包含三部分:

  • traceback header:

    Traceback (most recent call last):

    Traceback (innermost last):

  • traceback stack:中间的堆栈信息

  • exception:异常类型和细节

对于第二部分,由于堆栈信息是一直变化着的(比如,具体的文件或行号),所以 doctest 会忽略它。因此,定义期望的异常的最佳的方式是把中间的堆栈信息用省略号替代,比如上面的例子,可以定义成:

1
2
3
Traceback (most recent call last):
	...
TypeError: unhashable type: 'slice'

具体可参考 什么是 doctest 一节的例子。

 

参考

https://docs.python.org/3/library/doctest.html


- 全文完 -

相关文章

「 您的赞赏是激励我创作和分享的最大动力! 」