有时候我们想在 Python 中做一些测试,比如测试刚写的类的方法是否运行正常,或者测试代码是否产生预期异常。这种情况下使用 unittest 是麻烦且费事的,因为我们需要直观的在代码中看到这些测试,并且能运行这些测试。
doctest 模块为我们提供了一种在 Python docstring 中写测试用例并进行测试的方法。
在 Python 的官方文档中,对 doctest 的介绍是这样的:
doctest 模块会搜索那些看起来像是 Python 交互式会话中的代码片段,然后尝试执行并验证结果。
源码中的 doctest1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16# test_doctest.py
def multiply(x, y):
"""test multiply
>>> multiply(3, 4)
12
>>> multiply('x', 3)
'xxx'
"""
return x * y
if __name__ == '__main__':
import doctest
doctest.testmod(verbose=True)
测试:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18$ python doctest_test.py
Trying:
multiply(3, 4)
Expecting:
12
ok
Trying:
multiply('x', 3)
Expecting:
'xxx'
ok
1 items had no tests:
__main__
1 items passed all tests:
2 tests in __main__.multiply
2 tests in 2 items.
2 passed and 0 failed.
Test passed.
有两个地方可以放 doctest 的测试用例,一个位置是模块的开头,令一个位置是函数声明语句的下一行(如上例)。除此之外其他地方的 doctest 都无效,即使放了也不会被执行。
doctest 在 docstring 中寻找测试用例的时候,认为 >>> 是一个测试用例的开始,直到遇到空行或者下一个 >>>,在两个测试用例之间的其他内容会被 doctest 忽略掉。
当 __main__ 函数不方便调用 doctest 的时候,可以使用另一种执行方法:
1
2$ python -m doctest test_doctest.py
$ python -m doctest -v test_doctest.py
-v 参数用于输出详细信息。
独立文件中的 doctest
如果不想把 doctest 内嵌于 Python 源码中,可以建立一个独立文件来保存测试用例。
1
2
3
4
5
6
7
8
9
10
11test_doctest.txt
'>>>' 开头的行就是doctest测试用例。
不带 '>>>' 的行就是测试用例的输出。
如果实际运行的结果与期望的结果不一致,就标记为测试失败。
>>> from test_doctest import multiply
>>> multiply(3, 4)
12
>>> multiply('a', 3)
'aaa'
然后执行:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20$ python -m doctest -v test_doctest.txt
Trying:
from test_doctest import multiply
Expecting nothing
ok
Trying:
multiply(3, 4)
Expecting:
12
ok
Trying:
multiply('a', 3)
Expecting:
'aaa'
ok
1 items passed all tests:
3 tests in test_doctest.txt
3 tests in 1 items.
3 passed and 0 failed.
Test passed.
这里注意,from 一行也要以 >>> 开头。
处理可变变量
测试过程中,有些内容是不断变化的,如时间、对象ID等等。
1
2
3
4
5
6
7
8
9
10
11
12# test_changable.py
class T:
pass
def unpredictable(o):
"""return a new list contains object
>>> unpredictable(T())
[]
"""
return [o]
直接运行这个测试用例必然失败,因为对象在内存中的位置是不固定的。这个时候我们可以使用 doctest 的 ELLOPSIS 开关,并在需要忽略的地方用 ... 代替。
1
2
3
4
5
6
7
8
9
10
11
12# test_new_changable.py
class T:
pass
def unpredictable(o):
"""return a new list contains object
>>> unpredictable(T()) # doctest: +ELLIPSIS
[]
"""
return [o]
测试结果:
1
2
3
4
5
6
7
8
9
10
11
12
13
14$ python -m doctest test_new_changable.py -v
Trying:
unpredictable(T()) # doctest: +ELLIPSIS
Expecting:
[]
ok
2 items had no tests:
test_new_changable
test_new_changable.T
1 items passed all tests:
1 tests in test_new_changable.unpredictable
1 tests in 3 items.
1 passed and 0 failed.
Test passed.
交互器跨多行1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17# test_multiline.py
def group_by_length(words):
"""return a dictionary grouping words into sets by length
>>> grouped = group_by_length(['python', 'module', 'of', 'the', 'week'])
>>> grouped == {2: {'of'},
... 3: {'the'},
... 4: {'week'},
... 6: {'python', 'module'}
... }
True
"""
ret = {}
for word in words:
s = ret.setdefault(len(word), set())
s.add(word)
return ret
测试结果:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21$ python -m doctest test_multiline.py -v
Trying:
grouped = group_by_length(['python', 'module', 'of', 'the', 'week'])
Expecting nothing
ok
Trying:
grouped == {2: {'of'},
3: {'the'},
4: {'week'},
6: {'python', 'module'}
}
Expecting:
True
ok
1 items had no tests:
test_multiline
1 items passed all tests:
2 tests in test_multiline.group_by_length
2 tests in 2 items.
2 passed and 0 failed.
Test passed.
异常
Traceback 是一种特殊的可变变量,因为 Traceback 中的信息会随着系统平台、脚本文件位置变化而变化,所以匹配 Traceback 的时候我们需要忽略一些东西。可以只写第一行的 Traceback (most recent call last): 或者 Traceback (innermost last): 和最后一行的异常信息即可。
1
2
3
4
5
6
7
8
9
10# test_exception.py
def t():
"""raise a runtime error
>>> t()
Traceback (most recent call last):
...
RuntimeError: This is a runtime error!
"""
raise RuntimeError('This is a runtime error!')
测试结果:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15$ python -m doctest test_exception.py -v
Trying:
t()
Expecting:
Traceback (most recent call last):
...
RuntimeError: This is a runtime error!
ok
1 items had no tests:
test_exception
1 items passed all tests:
1 tests in test_exception.t
1 tests in 2 items.
1 passed and 0 failed.
Test passed.
处理空白字符
有时测试用例的输出中会有空行、空格等空白字符,然而 doctest 默认空行代表测试用例的结束,这是我们可以用 代表空行。
1
2
3
4
5
6
7
8
9
10
11
12# test_blankline.py
def double_space(lines):
"""
>>> double_space(['Line 1', 'Line 2'])
Line 1
Line 2
"""
for line in lines:
print(line)
print()
测试结果:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16$ python -m doctest -v test_blankline.py
Trying:
double_space(['Line 1', 'Line 2'])
Expecting:
Line 1
Line 2
ok
1 items had no tests:
test_blankline
1 items passed all tests:
1 tests in test_blankline.double_space
1 tests in 2 items.
1 passed and 0 failed.
Test passed.
关于 doctest 常用的就这么多,更多内容请参考官方文档。