打印

python单元测试框架学习总结

[复制链接]
3477|5
手机看帖
扫描二维码
随时随地手机跟帖
跳转到指定楼层
楼主
原野之狼|  楼主 | 2012-2-27 08:37 | 只看该作者 回帖奖励 |倒序浏览 |阅读模式
本帖最后由 原野之狼 于 2012-2-27 10:03 编辑

想了想,还是将就着把**贴在这个版块吧,其它版块貌似也不合适啊:)

 
                                 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 ------

相关帖子

沙发
原野之狼|  楼主 | 2012-2-27 08:59 | 只看该作者
悲剧了   **被自动阉割了:Q

使用特权

评论回复
板凳
原野之狼|  楼主 | 2012-2-27 09:55 | 只看该作者
搞定:lol

使用特权

评论回复
地板
huzaizai007| | 2012-2-27 10:02 | 只看该作者
lz玩python出于个人爱好?

使用特权

评论回复
5
原野之狼|  楼主 | 2012-2-27 10:08 | 只看该作者
4# huzaizai007
闲着也是闲着 玩玩嘛~

使用特权

评论回复
6
dong_abc| | 2012-2-27 20:38 | 只看该作者
好**!

使用特权

评论回复
发新帖 我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则

187

主题

8545

帖子

280

粉丝