Python 测试模块 doctest

有时候我们想在 Python 中做一些测试,比如测试刚写的类的方法是否运行正常,或者测试代码是否产生预期异常。这种情况下使用 unittest 是麻烦且费事的,因为我们需要直观的在代码中看到这些测试,并且能运行这些测试。

doctest 模块为我们提供了一种在 Python docstring 中写测试用例并进行测试的方法。

在 Python 的官方文档中,对 doctest 的介绍是这样的:

doctest 模块会搜索那些看起来像是 Python 交互式会话中的代码片段,然后尝试执行并验证结果。

源码中的 doctest

1
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
11
test_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())
[<test_changable.T object at 0x000002807892D390>]
"""
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
[<test_new_changable.T object at 0x...>]
"""
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:
[<test_new_changable.T object at 0x...>]
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 默认空行代表测试用例的结束,这是我们可以用 <BLANKLINE> 代表空行。

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
<BLANKLINE>
Line 2
<BLANKLINE>
"""
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
<BLANKLINE>
Line 2
<BLANKLINE>
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 常用的就这么多,更多内容请参考官方文档。

-------------本文结束感谢阅读-------------
  • 本文标题:Python 测试模块 doctest
  • 本文作者:xlui
  • 发布时间:2018年05月06日 - 21:05
  • 最后更新:2023年01月20日 - 01:01
  • 本文链接: https://xlui.me/t/python-doctest/
  • 版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-ND 4.0 许可协议。转载请注明出处!