31.Python的单元测试工具——doctest

转载请注明原始出处:http://blog.csdn.net/a464057216/article/details/51866748

后续此博客不再更新,欢迎大家搜索关注微信公众号“测开之美”,测试开发工程师技术修炼小站,持续学习持续进步。

doctest是一个python标准库自带的轻量单元测试工具,适合实现一些简单的单元测试。它可以在docstring中寻找测试用例并执行,比较输出结果与期望值是否符合。

  • 基本用法

使用doctest需要先在python的交互解释器中创建测试用例,并复制粘贴到docstring中即可。比如a.py内容如下:

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

    return a * b

然后使用如下命令执行测试:

$ python -m doctest a.py -v
Trying:
    my_function(2, 3)
Expecting:
    6
ok
Trying:
    my_function('a', 3)
Expecting:
    'aaa'
ok
1 items had no tests:
    a
1 items passed all tests:
   2 tests in a.my_function
2 tests in 2 items.
2 passed and 0 failed.
Test passed

doctest在docstring中寻找测试用例的时候,认为>>>是一个测试用例的开始,直到遇到空行或者下一个>>>,在两个测试用例之间有其他内容的话,会被doctest忽略(可以利用这个特性为测试用例编写一些注释)。

  • 处理可变变量

测试过程中,有一些结果的部分是在不断变化的,比如时间、对象的ID等等。b.py内容如下:

class MyClass(object):
    pass

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

    >>> unpredictable(MyClass())
    []
    """
    return [obj]

直接运行这个测试用例必然失败,因为对象在内存中的位置是不固定的,这个时候可以使用doctest的ELLIPSIS开关,并在需要忽略的地方用代替。c.py的内容如下:

class MyClass(object):
    pass

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

    >>> unpredictable(MyClass()) #doctest: +ELLIPSIS
    []
    """

    return [obj]

测试结果如下:

$ python -m doctest c.py -v
Trying:
    unpredictable(MyClass()) #doctest: +ELLIPSIS
Expecting:
    []
ok
2 items had no tests:
    c
    c.MyClass
1 items passed all tests:
   1 tests in c.unpredictable
1 tests in 3 items.
1 passed and 0 failed.
Test passed.
  • 期望值为空和交互器跨多行的情形

有的时候比如一个简单的赋值操作,是没有返回值的。另外如果有for循环等需要跨越多行的代码,也需要有正确的编写方式。d.py的内容如下:

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 = {}
    for word in words:
        s = d.setdefault(len(word), set())
        s.add(word)
    return d

测试结果如下:

$ python -m doctest d.py -v
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:
    d
1 items passed all tests:
   2 tests in d.group_by_length
2 tests in 2 items.
2 passed and 0 failed.
Test passed.
  • 处理Traceback

Traceback是一种特殊的可变变量,因为Traceback中的信息会随着系统平台、脚本文件位置的变化而变化,所以要匹配Traceback信息时,可以只写第一行Traceback (most recent call last):(或者Traceback (innermost last):)和最后一行异常类型及异常信息,忽略中间的路径信息等内容即可。e.py内容如下:

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

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

测试结果如下:

$ python -m doctest e.py -v
Trying:
  this_raises()
Expecting:
  Traceback (most recent call last):
  RuntimeError: here is the error
ok
1 items had no tests:
  e
1 items passed all tests:
  1 tests in e.this_raises
1 tests in 2 items.
1 passed and 0 failed.
Test passed.

把Traceback信息完全复制进来也没有关系,doctest在处理的时候其实只关注第一行和最后一行。

  • 处理空白字符

有时输出中会包含空行、空格等空白字符,然而在doctest的期望结果中,空行是表示测试用例的结束,这个时候可以用代表空行。f.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
    return

测试结果如下:

$ python -m doctest -v f.py
Trying:
  double_space(['Line one.', 'Line two.'])
Expecting:
  Line one.
  
  Line two.
  
ok
1 items had no tests:
  f
1 items passed all tests:
  1 tests in f.double_space
1 tests in 2 items.
1 passed and 0 failed.
Test passed.

另外一种需要处理空白的场景是期望值附近可能在复制粘贴过程中加入了不必要的空格字符,这个时候实际结果和期望结果肯定有差异,但是在测试报告中这种差异并不能一眼看出来。这种情况下可以使用doctest的REPORT_NDIFF选项,比如g.py的内容如下(6后面有一个多余的空格):

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

测试结果如下:

Trying:
    my_function(2, 3)
Expecting:
    6
**********************************************************************
File "g.py", line 3, in g.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:
    g
**********************************************************************
1 items had failures:
   1 of   2 in g.my_function
2 tests in 2 items.
1 passed and 1 failed.
***Test Failed*** 1 failures.

仅凭肉眼很难观察到到底是哪里有差异,使用REPORT_NDIFF:

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

测试结果如下:

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

有时,为了增强测试代码可读性,需要在期望值中加入一些空白字符,比如对于某些数据结构来说,分布在多行无疑可读性更强,这个时候可以使用doctest的NORMALIZE_WHITESPACE选项,比如h.py内容如下:

def my_function(dic):
    '''
    >>> my_function({1:2, 3:4}) #doctest:+NORMALIZE_WHITESPACE
    {1: 2,
     3: 4}
    '''
    return dic

测试结果如下:

$ python -m doctest -v h.py
Trying:
    my_function({1:2, 3:4}) #doctest:+NORMALIZE_WHITESPACE
Expecting:
    {1: 2,
     3: 4}
ok
1 items had no tests:
    h
1 items passed all tests:
   1 tests in h.my_function
1 tests in 2 items.
1 passed and 0 failed.
Test passed.

需要注意的是,使用NORMALIZE_WHITESPACE时,如果在实际输出没有空白字符的位置添加了空白,测试是无法通过的,比如上面的例子,在“{”右边并没有空格,如果h.py中添加了空格,会导致测试失败:

def my_function(dic):
    '''
    >>> my_function({1:2, 3:4}) #doctest:+NORMALIZE_WHITESPACE
    { 1: 2,
     3: 4}
    '''
    return dic

失败的测试结果如下:

$ python -m doctest -v h.py
Trying:
    my_function({1:2, 3:4}) #doctest:+NORMALIZE_WHITESPACE
Expecting:
    { 1: 2,
     3: 4}
**********************************************************************
File "h.py", line 3, in h.my_function
Failed example:
    my_function({1:2, 3:4}) #doctest:+NORMALIZE_WHITESPACE
Expected:
    { 1: 2,
     3: 4}
Got:
    {1: 2, 3: 4}
1 items had no tests:
    h
**********************************************************************
1 items had failures:
   1 of   1 in h.my_function
1 tests in 2 items.
0 passed and 1 failed.
***Test Failed*** 1 failures.
  • 测试用例的位置

目前所有的例子都是在函数的docstring中,实际上doctest的测试用例可以放在任何docstring中,比如模块、类、类的方法等的docstring中。
虽然doctest可以存在于那么多位置,但是这些测试用例在helpdoc中是可见的,为了在helpdoc中不可见,可以使用一个叫做__test__的模块级的字典变量,字典的键是一个字符串,用来表示测试用例集的名字,字典的值是一个包含了doctest的测试用例的字符串、模块、类或者函数,比如i.py和my_test_module.py内容分别如下:

# i.py
import my_test_module

def my_test_func():
    '''
    >>> my_func(['a', 'b'], 2)
    ['a', 'b', 'a', 'b']
    '''
    pass

class my_test_class(object):
    '''
    >>> my_func("lmz", 3)
    'lmzlmzlmz'
    '''
    pass

def my_func(a, b):
    '''
    Returns a multiply b times.
    '''
    return a * b

__test__ = {
    'function': my_test_func,
    'class': my_test_class,
    'module': my_test_module,
    'string': '''
    >>> my_func(13, 4)
    52
    '''
}

#my_test_module.py
'''
>>> my_func(2.0, 1)
2.0
'''

测试结果如下:

$ python -m doctest -v i.py
Trying:
    my_func(2.0, 1)
Expecting:
    2.0
ok
Trying:
    my_func(13, 4)
Expecting:
    52
ok
Trying:
    my_func("lmz", 3)
Expecting:
    'lmzlmzlmz'
ok
Trying:
    my_func(['a', 'b'], 2)
Expecting:
    ['a', 'b', 'a', 'b']
ok
2 items had no tests:
    i
    i.my_func
4 items passed all tests:
   1 tests in i.__test__.module
   1 tests in i.__test__.string
   1 tests in i.my_test_class
   1 tests in i.my_test_func
4 tests in 6 items.
4 passed and 0 failed.
Test passed.
  • 从其他格式的帮助文档中进行测试

比如使用reStructuredText帮助文档进行测试,如下是j.py和j.rst的内容:

#j.rst
===============================
How to use doctest in help doc
===============================

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 j 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, list, or tuple.

::

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

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

#j.py
def my_function(a, b):
    """
    Returns a*b
    """
    return a * b

注意测试开始需要先导入模块,执行测试后输出如下:

$ python -m doctest -v j.rst
Trying:
    from j 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 j.rst
5 tests in 1 items.
5 passed and 0 failed.
Test passed.

PS:reStructuredText是Python的一款文档工具,上面j.rst转换后的效果是这样的:

31.Python的单元测试工具——doctest_第1张图片

  • 更方便的执行测试
    目前为止都是使用doctest的命令行形式执行测试,如果我们自己开发的package包含很多文件的话,那么敲命令会很麻烦,下面来看看其他执行doctest测试用例的方法。

1.从模块执行
比如k.py的内容如下:

def my_function(a, b):
    '''
    >>> my_function(2, 3)
    6

    >>> my_function('lmz', 2)
    'lmzlmz'
    '''
    return a * b

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

这样直接使用如下命令就可以运行测试用例:

MarsLoo:learnspace marsloo$ python k.py  -v
Trying:
    my_function(2, 3)
Expecting:
    6
ok
Trying:
    my_function('lmz', 2)
Expecting:
    'lmzlmz'
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(模块名)。

2.从文件执行
类似从模块执行,doctest模块还有一个testfile(filename)的方法。

3.与unittest配合使用
doctest模块的DocTestSuite和DocFileSuite函数能够与unittest对接,比如:

import unittest, doctest
import j

suite = unittest.TestSuite()
suite.addTest(doctest.DocTestSuite(j))
suite.addTest(doctest.DocFileSuite('j.rst'))

runner = unittest.TextTestRunner(verbosity = 2)
runner.run(suite)
  • 关于变量空间
    doctest在执行每个测试用例的时候,会为他们创建隔离的变量空间,这样在测试用例A中创建的变量,在测试用例B中是访问不到的,比如:
# n.py
class MyClass(object):
    def case_A(self):
        '''
        >>> var = 'a'
        >>> 'var' in globals()
        True
        '''
        pass

    def case_B(self):
        '''
        >>> 'var' in globals()
        False
        '''
        pass

执行测试后结果如下:

MarsLoo:learnspace marsloo$ python -m doctest n.py -v
Trying:
    var = 'a'
Expecting nothing
ok
Trying:
    'var' in globals()
Expecting:
    True
ok
Trying:
    'var' in globals()
Expecting:
    False
ok
2 items had no tests:
    n
    n.MyClass
2 items passed all tests:
   2 tests in n.MyClass.case_A
   1 tests in n.MyClass.case_B
3 tests in 4 items.
3 passed and 0 failed.
Test passed.

如果实在想要在测试用例之间交换数据,可以在脚本的全局范围内放置一个字典,比如:

# o.py
__inner_dict__ = {}

class MyClass(object):
    def case_A(self):
        '''
        >>> global __inner_dict__
        >>> __inner_dict__['var'] = 'a'
        >>> 'var' in __inner_dict__
        True
        '''
        pass

    def case_B(self):
        '''
        >>> 'var' in __inner_dict__
        True
        '''
        pass

执行测试后结果如下:

MarsLoo:learnspace marsloo$ python -m doctest o.py -v
Trying:
    global __inner_dict__
Expecting nothing
ok
Trying:
    __inner_dict__['var'] = 'a'
Expecting nothing
ok
Trying:
    'var' in __inner_dict__
Expecting:
    True
ok
Trying:
    'var' in __inner_dict__
Expecting:
    True
ok
2 items had no tests:
    o
    o.MyClass
2 items passed all tests:
   3 tests in o.MyClass.case_A
   1 tests in o.MyClass.case_B
4 tests in 4 items.
4 passed and 0 failed.
Test passed.

如果觉得我的文章对您有帮助,欢迎关注我(CSDN:Mars Loo的博客)或者为这篇文章点赞,谢谢!

你可能感兴趣的:(python)