python单元测试框架原理探析
by: 原野之狼
email: y_y_z_l # 163.com
date: 2012年 02月 24日 星期五 17:50:56 CST
python自带包里面有个unittest.py模块,提供了python单元测试的基本框架,现在简要分析下。
该文件的头部有应用范例如下:
Simple usage:
import unittest
class IntegerArithmenticTestCase(unittest.TestCase):
def testAdd(self): ## test method names begin 'test*'
self.assertEquals((1 + 2), 3)
self.assertEquals(0 + 1, 1)
def testMultiply(self):
self.assertEquals((0 * 10), 0)
self.assertEquals((5 * 8), 40)
if __name__ == '__main__':
unittest.main()
其中有几个要点:
1、要使用unittest模块,须先导入它。 import unittest
2、自定义类需要继承自 unittest.TestCase 类
3、自定义类的方法名须以test字符作为前缀
我们先测试下上面的代码:
1、拷贝以上代码到一个新的文件:unittest_demo.py 记得在文件第一行键入:#!/usr/bin/env python
#!/usr/bin/env python
import unittest
class IntegerArithmenticTestCase(unittest.TestCase):
def testAdd(self): ## test method names begin 'test*'
self.assertEquals((1 + 2), 3)
self.assertEquals(0 + 1, 1)
def testMultiply(self):
self.assertEquals((0 * 10), 0)
self.assertEquals((5 * 8), 40)
if __name__ == '__main__':
unittest.main()
2、运行一下:
chmod 774 unittest_demo.py
./ unittest_demo.py
3、运行结果:
..
----------------------------------------------------------------------
Ran 2 tests in 0.000s
OK
以上信息表明,两个测试用例运行OK。
4、修改一下代码的内容:
self.assertEquals((1 + 2), 3)
改成
self.assertEquals((1 + 2), 4)
5、再运行下看看结果:
F.
======================================================================
FAIL: testAdd (__main__.IntegerArithmenticTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
File "./unittest_demo.py", line 7, in testAdd
self.assertEquals((1 + 2), 4)
AssertionError: 3 != 4
----------------------------------------------------------------------
Ran 2 tests in 0.001s
FAILED (failures=1)
以上信息表明出现了错误,并把错误信息打印出来了,这个对于分析测试Failure有非常大的帮助。
接下来分析下运行过程:
1、unittest_demo.py第14行:
unittest.main()
实际上是一个实例化对象的操作。从unittest.py的867行
main = TestProgram
可以看出main也就是TestProgram,它是一个类,因此 unittest.main() 实例化了一个TestProgram类型的对象。
2、看一下Line800 TestProgram 类(以下篇幅所使用的代码来自于unittest.py)的 __init__ 方法:
def __init__(self, module='__main__', defaultTest=None,
argv=None, testRunner=None,
testLoader=defaultTestLoader):
从步骤1可以发现,实例化过程中没有传递参数,因此 __init__ 方法采用默认参数。
那么Line804将得到运行:
self.module = __import__(module)
其导入了__main__模块,并存在对象属性self.module中。
Line809~815初始化了一些别的属性:
if argv is None:
argv = sys.argv
self.verbosity = 1
self.defaultTest = defaultTest
self.testRunner = testRunner
self.testLoader = testLoader
self.progName = os.path.basename(argv[0])
3、Line816调用了
self.parseArgs(argv)
方法,看看该方法做了什么?
其argv解析部分跟我们这个DEMO无关,重点看下Line837:
self.test = self.testLoader.loadTestsFromModule(self.module)
这行代码用于加载测试用例,把目标模块module(__main__)中的testcase(testAdd,testMultiply)载入框架的suite中。
从unittest_demo.py中可以看到,我们并没有实例化IntegerArithmenticTestCase类型的对象,但是测试用例testAdd、testMultiply确得到了执行,它是由框架调用的,由于 testAdd、testMultiply 并不是静态方法,所以上述代码肯定有实例化IntegerArithmenticTestCase类型的对象的操作,我们试着找找看。
4、self.testLoader已经初始化为一个TestLoader类型的实例,这个可以从步骤2以及Line628找到依据:
defaultTestLoader = TestLoader()
跟踪一下loadTestsFromModule方法做了什么,Line552~560:
def loadTestsFromModule(self, module):
"""Return a suite of all tests cases contained in the given module"""
tests = []
for name in dir(module):
obj = getattr(module, name)
if (isinstance(obj, (type, types.ClassType)) and
issubclass(obj, TestCase)):
tests.append(self.loadTestsFromTestCase(obj))
return self.suiteClass(tests)
从module中dir出所有的name符号,并以此为参数调用getattr获得该名字对应的obj,然后由if过滤出types.ClassType(类类型)以及TestCase子类,也就是说此时得到的obj是__main__.IntegerArithmenticTestCase,然后调用self.loadTestsFromTestCase()方法,得到IntegerArithmenticTestCase类型的实例,append到tests列表中,最后通过self.suiteClass(tests)实例化出一个TestSuite类型的实例并return。注意一下,tests作为构造函数的参数,在构造的过程中,Line411~413:
def __init__(self, tests=()):
self._tests = []
self.addTests(tests)
已经把测试用例tests(aIntegerArithmenticTestCase类型的实例)add到了TestSuite中,这样我们才能在框架中调用到目标测试用例,由于python语言的一些特性(import,dir,getattr,map等等),我们不需要显示地把目标测试用例加入框架中,只需要满足一定的命名规则(以test做前缀),那么框架运行起来后会自动地完成实例化过程以及关联过程,从而给极大地简化了用户的使用。这个优良的特性是c/c++语言测试框架所不具备的,当然并不是说c/c++不行,只是语言差异罢了,因为python是一门解析型的动态语言,而c/c++是编译型的静态语言。
5、有必要对上一步中self.loadTestsFromTestCase()方法再分析下,因为之前的描述不够具体,且在细节上的描述不够严谨,找到L543~550:
def loadTestsFromTestCase(self, testCaseClass):
"""Return a suite of all tests cases contained in testCaseClass"""
if issubclass(testCaseClass, TestSuite):
raise TypeError("Test cases should not be derived from TestSuite. Maybe you meant to derive from TestCase?")
testCaseNames = self.getTestCaseNames(testCaseClass)
if not testCaseNames and hasattr(testCaseClass, 'runTest'):
testCaseNames = ['runTest']
return self.suiteClass(map(testCaseClass, testCaseNames))
其中最关键的地方在最后一行:
首先,它通过map(testCaseClass, testCaseNames)实例化出一个或一组testCaseClass(也就是aIntegerArithmenticTestCase)类型的对象,并在构造的过程中传入了名字参数testCaseNames(它是一个序列(元组还是列表?应该是列表!我不是很确定~),在本DEMO中也就是“testAdd"和"testMultiply",注意"test"前缀的匹配分析是在之前的self.getTestCaseNames(testCaseClass)完成的。)
接着,用第一步构造的对象作为参数实例化了一个TestSuite类型的对象,并返回,TestSuite类有一个特殊方法,Line431~432:
def __iter__(self):
return iter(self._tests)
因此它可以在步骤4中实现append调用,此时需要用到迭代器。
6、以上深入分析的过程结束,返回到步骤3之后应该运行的代码,Line817:
self.runTests()
准备工作在之前已经完成了,此处应该运行测试用例了,我们看下以上方法的实现过程,Line851~865:
def runTests(self):
if self.testRunner is None:
self.testRunner = TextTestRunner
if isinstance(self.testRunner, (type, types.ClassType)):
try:
testRunner = self.testRunner(verbosity=self.verbosity)
except TypeError:
# didn't accept the verbosity argument
testRunner = self.testRunner()
else:
# it is assumed to be a TestRunner instance
testRunner = self.testRunner
result = testRunner.run(self.test)
sys.exit(not result.wasSuccessful())
关键代码是 result = testRunner.run(self.test),它传入TestSuite类型的对象,因此可以很容易的找到测试用例,之前花了很多篇幅讲的内容其最重要的地方就是构造该TestSuite对象的过程。
继续深入,看看 testRunner.run(self.test)做了啥,Line456~461:
def run(self, result):
for test in self._tests:
if result.shouldStop:
break
test(result)
return result
很容易看出,迭代调用test(result),test是什么?是IntegerArithmenticTestCase类型的对象,这个类继承自unittest.TestCase类,后者有__call__方法,Line299~300:
def __call__(self, *args, **kwds):
return self.run(*args, **kwds)
所以才能采用test()形式来调用,因此接下来就是self.run(*args, **kwds)方法调用了,Line264~297:
def run(self, result=None):
if result is None: result = self.defaultTestResult()
result.startTest(self)
testMethod = getattr(self, self._testMethodName)
try:
try:
self.setUp()
except KeyboardInterrupt:
raise
except:
result.addError(self, self._exc_info())
return
ok = False
try:
testMethod()
ok = True
except self.failureException:
result.addFailure(self, self._exc_info())
except KeyboardInterrupt:
raise
except:
result.addError(self, self._exc_info())
try:
self.tearDown()
except KeyboardInterrupt:
raise
except:
result.addError(self, self._exc_info())
ok = False
if ok: result.addSuccess(self)
finally:
result.stopTest(self)
流程很清晰了,主要调用的方法有:setUp,testMethod,tearDown,以及对于result的处理,这是单元测试框架的标准动作啦,俺就不再赘述了~
------THE END ------