Unittest 源码分析系列----- (一)unittest运行原理

本文详细解析unittest框架如何仅执行以test开头的方法,从main入口到TestProgram,再到TestLoader的loadTestsFromModule,展示了测试用例加载、suite创建和执行的全过程。重点剖析了如何筛选测试用例和测试套件的构建策略。
摘要由CSDN通过智能技术生成

疑问1、unittest框架中,是如何执行并运行以test开头的方法,而不运行不以test开头的方法?
疑问2、执行unittest.main() 后,具体发生了什么?

当我们写好测试用例,运行该测试用例,具体的运行原理是啥?通过分析源码,了解整个运行过程:从load测试用例到测试套件suite,到通过TextTestRunner去执行测试用例。
(注:我们将以test开头的方法,称为测试用例)

test_demo.py代码如下

import unittest

class ModuleTest1(unittest.TestCase):
    def test1(self):
        print 'test1'    
          
    def test2(self):
        print 'test2'

class ModuleTest2(unittest.TestCase):
    def test3(self):
        print 'test3'
       
    def test4(self):
        print 'test4'

unittest.main()
python test_demo.py

当运行python test_demo.py ,则会运行unittest.main().

step1 : unittest.main()

通过unittest的源码,我们发现main.py中,

main = TestProgram

即unittest.main() = unittest.TestProgram() #即TestProgram类的实例对象

step2: TestProgram中__init__()

创建完TestProgram类的实例对象后,运行TestProgram类中的构造函数__init__()方法
TestProgram类的__init__()方法源码如下:

    def __init__(self, module='__main__', defaultTest=None, argv=None,
                    testRunner=None, testLoader=loader.defaultTestLoader,
                    exit=True, verbosity=1, failfast=None, catchbreak=None,
                    buffer=None):
        if isinstance(module, basestring):
            self.module = __import__(module)
            for part in module.split('.')[1:]:
                self.module = getattr(self.module, part)
        else:
            self.module = module
        if argv is None:
            argv = sys.argv

        self.exit = exit
        self.failfast = failfast
        self.catchbreak = catchbreak
        self.verbosity = verbosity
        self.buffer = buffer
        self.defaultTest = defaultTest
        self.testRunner = testRunner
        self.testLoader = testLoader
        self.progName = os.path.basename(argv[0])
        self.parseArgs(argv)
        self.runTests()

通过上述的源码:核心点在self.parseArgs(argv)和self.runTests()。

self.parseArgs(argv)
self.runTests()
step 2.1. self.parseArgs(argv)

parseArgs(argv)源码如下:


    def parseArgs(self, argv):
        .....省略....
            else:
                self.testNames = (self.defaultTest,)
            self.createTests()
        except getopt.error, msg:
            self.usageExit(msg)

通过如上源码,多余的不看,核心点在于self.createTests():

self.createTests()

self.createTests()源码如下:

def createTests(self):
    if self.testNames is None:
        self.test = self.testLoader.loadTestsFromModule(self.module)
    else:
        self.test = self.testLoader.loadTestsFromNames(self.testNames,
                                                        self.module)

从createTests()方法中,通过前面的代码,我们可以判定self.testNames是没有值的,故运行self.testLoader.loadTestsFromModule(self.module)。

self.testLoader.loadTestsFromModule(self.module)

通过TestProgram类中构造函数__init__()中,self.testLoader = testLoader—>testLoader=loader.defaultTestLoader---->load.py 文件中defaultTestLoader:

def __init__(self, module='__main__', defaultTest=None, argv=None,
                testRunner=None, testLoader=loader.defaultTestLoader,
                exit=True, verbosity=1, failfast=None, catchbreak=None,
                buffer=None):
                
                ....省略....

通过load.py文件中我们发现defaultTestLoader = TestLoader(),所以

self.testLoader.loadTestsFromModule(self.module)  ->等价于
loader.defaultTestLoader.loadTestsFromModule(self.module) ->等价于
loader.TestLoader().loadTestsFromModule(self.module)

故,运行Tload.py文件中TestLoader()类中的loadTestsFromModule()方法。

Step3 : TestLoader中loadTestsFromModule(self.module) 得到一个测试套件suite

loadTestsFromModule()源码如下:

def loadTestsFromModule(self, module, use_load_tests=True):
    """Return a suite of all test cases contained in the given module"""
    tests = []
    for name in dir(module): 
        obj = getattr(module, name) 
        if isinstance(obj, type) and issubclass(obj, case.TestCase):
            tests.append(self.loadTestsFromTestCase(obj))     
    load_tests = getattr(module, 'load_tests', None)
    tests = self.suiteClass(tests)
    if use_load_tests and load_tests is not None:
        try:
            return load_tests(self, tests, None)
        except Exception, e:
            return _make_failed_load_tests(module.__name__, e,
                                            self.suiteClass)
    return tests

核心1 : 代码分析:

for name in dir(module): #for 循环遍历
    #module即模块名,此时就是test_demo,dir(test_demo),返回test_demo模块下所有属性、方法、类,及相应的值
    obj = getattr(module, name) #返回test_demo中name属性的值,obj
    if isinstance(obj, type) and issubclass(obj, case.TestCase):#判别出obj是不是type ,是不是测试用例类TestCaseClass,并且继承unittest.TestCase类,目的:筛选出测试用例类,test_demo中ModuleTest1类和ModuleTest2类
        tests.append(self.loadTestsFromTestCase(obj)) 
#如果obj是测试用例类TestCaseClass,并继承了承unittest.TestCase类,则调用self.loadTestsFromTestCase(obj),并将其返回的结果append到tests列表中

核心2 :

tests = self.suiteClass(tests)

核心1 : 代码具体调用、运行过程分析

Step4 : self.loadTestsFromTestCase(obj)

源码如下:

def loadTestsFromTestCase(self, testCaseClass):
    """Return a suite of all test cases contained in testCaseClass"""
    if issubclass(testCaseClass, suite.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']
    loaded_suite = self.suiteClass(map(testCaseClass, testCaseNames))
    return loaded_suite

step1 :先调用

testCaseNames = self.getTestCaseNames(testCaseClass)

getTestCaseNames()方法,将测试用例类TestCaseClass中测试用例名(以test开头),以列表的形式返回,(即:test_demo.py 中,ModuleTest1 / or ModuleTest2中的[test1,test2] / or [test3,test4]).

调用testFnNames = filter(isTestMethod, dir(testCaseClass)),用filter()方法,把testCaseClass中所有属性和方法,去匹配isTestMethod()方法,筛出所有测试用例名(以test开头)。
为啥一定是test开头法方法才是测试用例,才会被执行,而其他方法(不以test开头),不作为测试用例,不执行,由prefix=self.testMethodPrefix决定,通过源码,我们可以看到TestLoader()上定义了类属性,

testMethodPrefix = 'test'
def getTestCaseNames(self, testCaseClass):
    """Return a sorted sequence of method names found within testCaseClass
    """
    def isTestMethod(attrname, testCaseClass=testCaseClass,
                        prefix=self.testMethodPrefix):
        return attrname.startswith(prefix) and \
            hasattr(getattr(testCaseClass, attrname), '__call__')
    testFnNames = filter(isTestMethod, dir(testCaseClass))
    if self.sortTestMethodsUsing:
        testFnNames.sort(key=_CmpToKey(self.sortTestMethodsUsing))
    return testFnNames

step2 :调用

loaded_suite = self.suiteClass(map(testCaseClass, testCaseNames))

self.suiteClass() -> 由suiteClass = suite.TestSuite ->得出suite.TestSuite(map(testCaseClass, testCaseNames))
将测试用例类(即,ModuleTest1,/or ModuleTest2),测试用例名list(即[test1,test2]] / or [ test3,test4]),通过map映射,并成为一个测试套件suite。
运行返回的结果如下:

如果是ModuleTest1,loaded_suite =

<unittest.suite.TestSuite tests=[<__main__.ModuleTest1 testMethod=test1>, <__main__.ModuleTest1 testMethod=test2>]>

或者ModuleTest2, loaded_suite =

<unittest.suite.TestSuite tests=[<__main__.ModuleTest2 testMethod=test3>, <__main__.ModuleTest1 testMethod=test3>]>

总结:self.loadTestsFromTestCase(obj) 返回的是由测试用例类与测试用例映射后,并形成一个测试套件suite。

回到loadTestsFromModule()中,将测试套件append到tests列表:

tests.append(self.loadTestsFromTestCase(obj))

因为前面有for循环,故得出的结果,将每一个测试用例类与它下面的测试用例映射后,创建测试套件suite,当成tests list中的元素,最终tests表里有好多个元素,每个元素都是一个测试套件suite,每个suite对应一个测试用例类与它的测试用例。

以test_demo.py 文件下的测试用例类与测试用例,为例:
tests = [
<unittest.suite.TestSuite tests=[<__main__.ModuleTest1 testMethod=test1>, <__main__.ModuleTest1 testMethod=test2>]>,
<unittest.suite.TestSuite tests=[<__main__.ModuleTest2 testMethod=test3>, <__main__.ModuleTest2 testMethod=test4>]>   
 ]

核心代码2:

tests = self.suiteClass(tests)

将tests列表又变成一个测试用例suite。

以test_demo.py 文件下的测试用例类与测试用例,为例:
 tests =
 <unittest.suite.TestSuite tests=
[
 <unittest.suite.TestSuite tests=[<__main__.ModuleTest1 testMethod=testhah1>, <__main__.ModuleTest1 testMethod=testtwo2>]>,
 <unittest.suite.TestSuite tests=[<__main__.ModuleTest2 testMethod=testhah2>, <__main__.ModuleTest2 testMethod=testtwo2>]>
 ]
 >
self.runTests() 主要实现如何 执行测试用例

源码如下:

def runTests(self):
    if self.catchbreak:
        installHandler()
    if self.testRunner is None:
        self.testRunner = runner.TextTestRunner
    if isinstance(self.testRunner, (type, types.ClassType)):
        try:
            testRunner = self.testRunner(verbosity=self.verbosity,
                                            failfast=self.failfast,
                                            buffer=self.buffer)
        except TypeError:
            # didn't accept the verbosity, buffer or failfast arguments
            testRunner = self.testRunner()
    else:
        # it is assumed to be a TestRunner instance
        testRunner = self.testRunner
    self.result = testRunner.run(self.test)
    if self.exit:
        sys.exit(not self.result.wasSuccessful())

由上可得self.result = testRunner.run(self.test), self.test = loadTestsFromModule()中返回值 tests, 调用了TextTestRunner中的run()方法:

def run(self, test):
    "Run the given test case or test suite."
    result = self._makeResult()
    registerResult(result)
    result.failfast = self.failfast
    result.buffer = self.buffer
    startTime = time.time()
    startTestRun = getattr(result, 'startTestRun', None)
    if startTestRun is not None:
        startTestRun()
    try:
        test(result)  #test是个TestSuite的实例对象suite,test(),会调用TestSuite类中的__call__()方法
        .....其他代码省略....

TestSuite类中的__call__()方法,调用TestSuite中的run()方法:

def __call__(self, *args, **kwds):  #调用 TestSuite中的run()方法
    return self.run(*args, **kwds)
def run(self, result, debug=False):
	.....代码省略...
    for test in self:  #通过一个for循环依次调用TestCase的run方法去执行具体的测试用例(这里要注意的是,如果test还是TestSuite类型,会继续递归调用)。
        if result.shouldStop:
            break
        if _isnotsuite(test):
            self._tearDownPreviousClass(test, result)
            self._handleModuleFixture(test, result)
            self._handleClassSetUp(test, result)
            result._previousTestClass = test.__class__

            if (getattr(test.__class__, '_classSetupFailed', False) or
                getattr(result, '_moduleSetUpFailed', False)):
                continue

        if not debug:
            test(result)  #调用TestCase中—__call__(),即调用TestCase中run()函数
        else:
            test.debug()

    if topLevel:
        self._tearDownPreviousClass(None, result)
        self._handleModuleTearDown(result)
        result._testRunEntered = False
    return result

通过一个for循环,依次调用TestCase的run方法去执行具体的测试用例(这里要注意的是,如果test还是TestSuite类型,会继续递归调用)。
如何理解递归调用?

 for test in self:
#type(test) 测试用例test***对应的类
#self :是个测试suite,遍历这个suite,这个suite里面可能还有suite,会继续递归调用。
#<举例:
#第一次遍历
# self:unittest.suite.TestSuite tests=[<unittest.suite.TestSuite tests=[<__main__.ModuleTest1 testMethod=test1>, <__main__.ModuleTest1 testMethod=test2>]>, <unittest.suite.TestSuite tests=[<__main__.ModuleTest2 testMethod=test3>, <__main__.ModuleTest2 testMethod=test4>]>]>
#test:<unittest.suite.TestSuite tests=[<__main__.ModuleTest1 testMethod=test1>, <__main__.ModuleTest1 testMethod=test2>]>
#因为test还是个suite,则此时,递归,self=<unittest.suite.TestSuite tests=[<__main__.ModuleTest1 testMethod=test1>, <__main__.ModuleTest1 testMethod=test4>]>
#test=test1 (__main__.ModuleTest1),运行之后的代码,并运行test用例test1,得出结果
#下一次取值:test= test2 (__main__.ModuleTest1)
#再一次:<unittest.suite.TestSuite tests=[<__main__.ModuleTest2 testMethod=test3>, <__main__.ModuleTest2 testMethod=test4>]>
# .....

再运行

test(result)  #调用TestCase中—__call__(),即调用TestCase中run()函数

TestCase中run()函数
源码如下:

def run(self, result=None):
    orig_result = result
    if result is None:
        result = self.defaultTestResult()
        startTestRun = getattr(result, 'startTestRun', None)
        if startTestRun is not None:
            startTestRun()

    self._resultForDoCleanups = result
    result.startTest(self)

    testMethod = getattr(self, self._testMethodName)   #获取测试用例
    ....代码省略...
    ..........
	testMethod()   #执行测试用例
	....................
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值