Python代码测试 - unitest\doctest\nose

http://blog.csdn.net/pipisorry/article/details/39123651

一、使用doctest\unitest进行python代码测试

        对于开发者来说,最实用的帮助莫过于帮助他们编写代码文档了。pydoc模块可以根据源代码中的docstrings为任何可导入模块生成格式良好的文档。

Python包含了两个测试框架来自动测试代码以及验证代码的正确性:

1)doctest模块,该模块可以从源代码或独立文件的例子中抽取出测试用例。

2)unittest模块,该模块是一个全功能的自动化测试框架,该框架提供了对测试准备(test fixtures), 预定义测试集(predefined test suite)以及测试发现(test discovery)的支持。

unitest

通用测试框架[python单元测试unittest]


doctest

简单一点的模块,用于检查文档的,对于编写的单元测试也很在行。

doctest查找和执行交互式Python会话,检验其符合预期,常用场景如下:

  • 检验模块的文档字符串是最新的。
  • 衰退测试。
  • 书写教程。

Doctest比unittest简单,没有API,是大型测试框架的有机补充。不过doctest没有fixture,不适合复杂的场景。

[doctest — Test interactive Python examples]

[doctest – Testing through documentation]

官方文档提到的应用情景:
1.通过验证例子(doctest)检查模块的docstring是最新的。
有时候会出现代码已经改变但docstring没有更新的情况,在docstring中加入doctest可以尽量避免这种情况的发生。
2.回归测试
我的理解是当测试未通过的时候,可以把用例写在docstring里,可以方便的进行回归测试。
3.作为包或库的教程示例
一个可以执行的示例比大段的说明性文字更直观有效

doctest小示例

def unit_test(x):
    '''
    >>> unit_test(3)
    6
    '''
    return x*x

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

def square(x):

'''''

Squares a number and returns the results.

>>> square(2)

4

>>> square(3)

9

'''

return x*x



if __name__ == '__main__':

import doctest, my_math

doctest.testmod(my_math)

成功运行情况

[root@pdns0 PythonStudy]# python my_math.py -v

Trying:

square(2)

Expecting:

4

ok

Trying:

square(3)

Expecting:

9

ok

1 items had no tests:

my_math

1 items passed all tests:

2 tests in my_math.square

2 tests in 2 items.

2 passed and 0 failed.

Test passed.

把x*x改成x**x,得到错误运行情况:

[root@pdns0 PythonStudy]# python my_math.py -v

Trying:

square(2)

Expecting:

4

ok

Trying:

square(3)

Expecting:

9

**********************************************************************

File "/home/liqing/PythonStudy/my_math.py", line 8, in my_math.square

Failed example:

square(3)

Expected:

9

Got:

27

1 items had no tests:

my_math

**********************************************************************

1 items had failures:

1 of 2 in my_math.square

2 tests in 2 items.

1 passed and 1 failed.

***Test Failed*** 1 failures


Note:

1.">>>"与测试代码之间有个空格。
2.期望的测试结果与docstring之间要有一个空行。
    >>> add(1, 2)
    3
    a docstring   #这个会被认为是测试输出的一部分
应该写成这样
    >>> add(1, 2)
    3
 
    a docstring
3.python thefile.py -v
显示doctest详细信息

python2.6版本后,还可以这样执行doctest
python -m doctest -v thefile.py 

文档字符串与doctest模块

如果函数,类或者是模块的第一行是一个字符串,那么这个字符串就是一个文档字符串。可以认为包含文档字符串是一个良好的编程习惯,这是因为这些字符串可以给Python程序开发工具提供一些信息。比如,help()命令能够检测文档字符串,Python相关的IDE也能够进行检测文档字符串的工作。由于程序员倾向于在交互式shell中查看文档字符串,所以最好将这些字符串写的简短一些。例如

# mult.py
class Test:
    """
    >>> a=Test(5)
    >>> a.multiply_by_2()
    10
    """
    def __init__(self, number):
        self._number=number
 
    def multiply_by_2(self):
        return self._number*2
在编写文档时,一个常见的问题就是如何保持文档和实际代码的同步。例如,程序员也许会修改函数的实现,但是却忘记了更新文档。针对这个问题,我们可以使用doctest模块。doctest模块收集文档字符串,并对它们进行扫描,然后将它们作为测试进行执行。为了使用doctest模块,我们通常会新建一个用于测试的独立的模块。例如,如果前面的例子 Test class包含在文件 mult.py中,那么,你应该新建一个 testmult.py文件用来测试,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# testmult.py
 
import mult, doctest
 
doctest.testmod(mult, verbose = True )
 
# Trying:
#     a=Test(5)
# Expecting nothing
# ok
# Trying:
#     a.multiply_by_2()
# Expecting:
#     10
# ok
# 3 items had no tests:
#     mult
#     mult.Test.__init__
#     mult.Test.multiply_by_2
# 1 items passed all tests:
#    2 tests in mult.Test
# 2 tests in 4 items.
# 2 passed and 0 failed.
# Test passed.

在这段代码中,doctest.testmod(module)会执行特定模块的测试,并且返回测试失败的个数以及测试的总数目。如果所有的测试都通过了,那么不会产生任何输出。否则的话,你将会看到一个失败报告,用来显示期望值和实际值之间的差别。如果你想看到测试的详细输出,你可以使用testmod(module, verbose=True).

如果不想新建一个单独的测试文件的话,那么另一种选择就是在文件末尾包含相应的测试代码:

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

如果想执行这类测试的话,我们可以通过-m选项调用doctest模块。通常来讲,当执行测试的时候没有任何的输出。如果想查看详细信息的话,可以加上-v选项。

1
$ python - m doctest - v mult.py

单元测试与unittest模块

如果想更加彻底地对程序进行测试,我们可以使用unittest模块。通过单元测试,开发者可以为构成程序的每一个元素(例如,独立的函数,方法,类以及模块)编写一系列独立的测试用例。当测试更大的程序时,这些测试就可以作为基石来验证程序的正确性。当我们的程序变得越来越大的时候,对不同构件的单元测试就可以组合起来成为更大的测试框架以及测试工具。这能够极大地简化软件测试的工作,为找到并解决软件问题提供了便利。

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
# splitter.py
import unittest
 
def split(line, types = None , delimiter = None ):
     """Splits a line of text and optionally performs type conversion.
     ...
     """
     fields = line.split(delimiter)
     if types:
         fields = [ ty(val) for ty,val in zip (types,fields) ]
     return fields
 
class TestSplitFunction(unittest.TestCase):
     def setUp( self ):
         # Perform set up actions (if any)
         pass
     def tearDown( self ):
         # Perform clean-up actions (if any)
         pass
     def testsimplestring( self ):
         r = split( 'GOOG 100 490.50' )
         self .assertEqual(r,[ 'GOOG' , '100' , '490.50' ])
     def testtypeconvert( self ):
         r = split( 'GOOG 100 490.50' ,[ str , int , float ])
         self .assertEqual(r,[ 'GOOG' , 100 , 490.5 ])
     def testdelimiter( self ):
         r = split( 'GOOG,100,490.50' ,delimiter = ',' )
         self .assertEqual(r,[ 'GOOG' , '100' , '490.50' ])
 
# Run the unittests
if __name__ = = '__main__' :
     unittest.main()
 
#...
#----------------------------------------------------------------------
#Ran 3 tests in 0.001s
 
#OK

在使用单元测试时,我们需要定义一个继承自unittest.TestCase的类。在这个类里面,每一个测试都以方法的形式进行定义,并都以test打头进行命名——例如,’testsimplestring‘,’testtypeconvert‘以及类似的命名方式(有必要强调一下,只要方法名以test打头,那么无论怎么命名都是可以的)。在每个测试中,断言可以用来对不同的条件进行检查。

测试标准输出的例子:

假如你在程序里有一个方法,这个方法的输出指向标准输出(sys.stdout)。这通常意味着是往屏幕上输出文本信息。如果你想对你的代码进行测试来证明这一点,只要给出相应的输入,那么对应的输出就会被显示出来。

1
2
3
4
5
# url.py
 
def urlprint(protocol, host, domain):
     url = '{}://{}.{}' . format (protocol, host, domain)
     print (url)

内置的print函数在默认情况下会往sys.stdout发送输出。为了测试输出已经实际到达,你可以使用一个替身对象对其进行模拟,并且对程序的期望值进行断言。unittest.mock模块中的patch()方法可以只在运行测试的上下文中才替换对象,在测试完成后就立刻返回对象原始的状态。下面是urlprint()方法的测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#urltest.py
 
from io import StringIO
from unittest import TestCase
from unittest.mock import patch
import url
 
class TestURLPrint(TestCase):
     def test_url_gets_to_stdout( self ):
         protocol = 'http'
         host = 'www'
         domain = 'example.com'
         expected_url = '{}://{}.{}\n' . format (protocol, host, domain)
 
         with patch( 'sys.stdout' , new = StringIO()) as fake_out:
             url.urlprint(protocol, host, domain)
             self .assertEqual(fake_out.getvalue(), expected_url)

urlprint()函数有三个参数,测试代码首先给每个参数赋了一个假值。变量expected_url包含了期望的输出字符串。为了能够执行测试,我们使用了unittest.mock.patch()方法作为上下文管理器,把标准输出sys.stdout替换为了StringIO对象,这样发送的标准输出的内容就会被StringIO对象所接收。变量fake_out就是在这一过程中所创建出的模拟对象,该对象能够在with所处的代码块中所使用,来进行一系列的测试检查。当with语句完成时,patch方法能够将所有的东西都复原到测试执行之前的状态,就好像测试没有执行一样,而这无需任何额外的工作。但对于某些Python的C扩展来讲,这个例子却显得毫无意义,这是因为这些C扩展程序绕过了sys.stdout的设置,直接将输出发送到了标准输出上。这个例子仅适用于纯Python代码的程序(如果你想捕获到类似C扩展的输入输出,那么你可以通过打开一个临时文件然后将标准输出重定向到该文件的技巧来进行实现)。


二、使用nose进行python代码测试

推荐使用nose或是py.test,它们基本上是类似的。

使用nose进行测试的例子

在一个以test_开头的文件中的所有以test_开头的函数,都会被调用

1
2
def test_equality():
     assert True = = False

不出所料,当运行nose的时候,我们的测试没有通过。

1
2
3
4
5
6
7
8
9
10
11
12
13
( test )jhaddad@jons-mac-pro ~VIRTUAL_ENV /src $ nosetests                                                                                                                                     
F
======================================================================
FAIL: test_nose_example.test_equality
----------------------------------------------------------------------
Traceback (most recent call last):
   File "/Users/jhaddad/.virtualenvs/test/lib/python2.7/site-packages/nose/case.py" , line 197, in runTest
     self. test (*self.arg)
   File "/Users/jhaddad/.virtualenvs/test/src/test_nose_example.py" , line 3, in test_equality
     assert True == False
AssertionError
 
----------------------------------------------------------------------

nose.tools中同样也有一些便捷的方法可以调用

1
2
3
from nose.tools import assert_true
def test_equality():
     assert_true( False )

想使用更加类似JUnit的方法

1
2
3
4
5
6
7
8
9
10
from nose.tools import assert_true
from unittest import TestCase
 
class ExampleTest(TestCase):
 
     def setUp( self ): # setUp & tearDown are both available
         self .blah = False
 
     def test_blah( self ):
         self .assertTrue( self .blah)

开始测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
( test )jhaddad@jons-mac-pro ~VIRTUAL_ENV /src $ nosetests                                                                                                                                     
F
======================================================================
FAIL: test_blah (test_nose_example.ExampleTest)
----------------------------------------------------------------------
Traceback (most recent call last):
   File "/Users/jhaddad/.virtualenvs/test/src/test_nose_example.py" , line 11, in test_blah
     self.assertTrue(self.blah)
AssertionError: False is not true
 
----------------------------------------------------------------------
Ran 1 test in 0.003s
 
FAILED (failures=1)

卓越的Mock库包含在Python 3 中,但是如果你在使用Python 2,可以使用pypi来获取。这个测试将进行一个远程调用,但是这次调用将耗时10s。这个例子显然是人为捏造的。我们使用mock来返回样本数据而不是真正的进行调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import mock
 
from mock import patch
from time import sleep
 
class Sweetness( object ):
     def slow_remote_call( self ):
         sleep( 10 )
         return "some_data" # lets pretend we get this back from our remote api call
 
def test_long_call():
     s = Sweetness()
     result = s.slow_remote_call()
     assert result = = "some_data"

当然,我们的测试需要很长的时间。

1
2
3
4
5
( test )jhaddad@jons-mac-pro ~VIRTUAL_ENV /src $ nosetests test_mock.py                                                                                                                        
 
Ran 1 test in 10.001s
 
OK

太慢了!因此我们会问自己,我们在测试什么?我们需要测试远程调用是否有用,还是我们要测试当我们获得数据后要做什么?大多数情况下是后者。让我们摆脱这个愚蠢的远程调用吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import mock
 
from mock import patch
from time import sleep
 
class Sweetness( object ):
     def slow_remote_call( self ):
         sleep( 10 )
         return "some_data" # lets pretend we get this back from our remote api call
 
def test_long_call():
     s = Sweetness()
     with patch. object (s, "slow_remote_call" , return_value = "some_data" ):
         result = s.slow_remote_call()
     assert result = = "some_data"

好吧,让我们再试一次:

1
2
3
4
5
6
( test )jhaddad@jons-mac-pro ~VIRTUAL_ENV /src $ nosetests test_mock.py                                                                                                                        
.
----------------------------------------------------------------------
Ran 1 test in 0.001s
 
OK

好多了。记住,这个例子进行了荒唐的简化。就我个人来讲,我仅仅会忽略从远程系统的调用,而不是我的数据库调用。

nose-progressive是一个很好的模块,它可以改善nose的输出,让错误在发生时就显示出来,而不是留到最后。如果你的测试需要花费一定的时间,那么这是件好事。
pip install nose-progressive 并且在你的nosetests中添加--with-progressive

[ 写给已有编程经验的 Python 初学者的总结]
from: http://blog.csdn.net/pipisorry/article/details/39123651

ref:doctest样例

Python模块——doctest


你可能感兴趣的:(python,单元测试,unittest,nose,doctest)