深入解读Python的unittest并拓展HTMLTestRunner
unnitest
就是unnitest=TestCase+TestResult
,到其实最终执行的是 TestCase
中的run
方法,并把结果给 TestResult
(或它的子类)。
import unittest
class Mydemo(unittest.TestCase):
def setUp(self):
self.a=1
def test1(self):
print "i am test1 the value of a is {}".format(self.a)
def test2(self):
print "i am test2 the value of a is {}".format(self.a)
def test3(self):
print "i am test3 the value of a is {}".format(self.a)
if __name__ == '__main__':
unittest.main()
i am test1 the value of a is 1
...
i am test2 the value of a is 1
i am test3 the value of a is 1
----------------------------------------------------------------------
Ran 3 tests in 0.000s
OK
这个是没有问题的,那么我们可能要想这个unnitest.main()
是什么东西,还有其他的写法来执行吗,能只执行test1
,test2
,不执行test3
吗(暂时不用skip)?那么我们从unittest.main()
看起来。debug进入其实最终执行的是TestProgram这类,贴出构造函数部分代码:
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)#查找当前module的Testsuite
self.runTests()#执行测试
从上面我们可以看出来其实也就2个主要的步骤就是第一:找出要测试的testcase,并加入到Testsuite,第二:运行Testsuite并把结果给TestResult。
首先,第一:了解什么是TestCase?什么是TestSuite?第二:如果找出这些Testcase,或者TestSuite?
有人说TesetCase
就是以test开头的就叫一个testcase
,准确的说:是实例了一个TesetCase
类的叫一个TestCase
,比如这样:
import unittest
class Mydemo(unittest.TestCase):
def setUp(self):
self.a=1
def Mytest1(self):
print "i am Mytest1 the value of a is {}".format(self.a)
def Mytest2(self):
print "i am Mytest2 the value of a is {}".format(self.a)
def Mytest3(self):
print "i am Mytest3 the value of a is {}".format(self.a)
if __name__ == '__main__':
test_runner=unittest.TextTestRunner()
test_suit=unittest.TestSuite()
test_suit.addTests(map(Mydemo,["Mytest1","Mytest2","Mytest3"]))
test_runner.run(test_suit)
...
i am Mytest1 the value of a is 1
----------------------------------------------------------------------
i am Mytest2 the value of a is 1
Ran 3 tests in 0.000s
i am Mytest3 the value of a is 1
OK
上面3个Testcase
可并没有以test开头…那么为什么大家都要默认以test开头来写呢,我们打开C:\Python27\Lib\unittest\loader.py
这个模块在296行有写defaultTestLoader = TestLoader()
,我们来看看TestLoader
这个类第一行就看见testMethodPrefix = 'test'
,也就是说如果你使用到defaultTestLoader
,那么默认是以test开头的方法为一个用例,具体可以在TestLoader
类中的getTestCaseNames
得到实现,红字注释部分为什么testCaseClass
要有__call__
方法,我们后面提到。(不知道__call__
这个魔法属性的用法自行百度)
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__')#返回一个testCaseClass有__call__方法且attrname以prefix开头的为一个testcase
testFnNames = filter(isTestMethod, dir(testCaseClass))
if self.sortTestMethodsUsing:
testFnNames.sort(key=_CmpToKey(self.sortTestMethodsUsing))
return testFnNames
原来是这样啊,我们上文提到的unittest.main()
其实用的就是defaultTestLoader
,当然你把if __name__ == '__main__'
下面的代码换成unittest.main()
肯定不成功,除非你把上文提到的testMethodPrefix
换成"Mytest"
。有了对TestCase
的看法,我们具体来看看这个类。
这个类里面包含了我们所能用的方法。我列出来一些主要的吧。
setUp()
在每个test执行前都要执行的方法。
tearDown()
在每个test执行后都要执行的方法。(不管是否执行成功)
setUpClass()
在一个测试类中在所有test开始之前,执行一次且必须使用到Testsuite
(只有在TestSuite
的run方法里面才对其调用)
tearDownClass()
在一个测试类中在所有test结束之后,执行一次且必须使用到Testsuite
(只有在TestSuite的run方法里面才对其调用)
run()
这是unnitest
的核心,逻辑也相对复杂,但是很好理解,具体自己看源码。所有最终case的执行都会归结到该run方法。
还有一个重要的_resultForDoCleanups
私有变量,存储TestResult
的执行结果,这个在构建后面的skip
用到。
我们要明确TestCase类中所有的测试用例是独立的,我上面说过了,其实每个testcase就是一个个TestCase
类的实例对象,所以不要企图在某个test存储或改变一个变量,下个test中能用到,除非利用到setUpClass
。我们看个例子:
import unittest
class Mydemo(unittest.TestCase):
def test1(self):
self.a=1
print "i am test1 the value of a is {}".format(self.a)
def test2(self):
print "i am test2 the value of a is {}".format(self.a)
if __name__ == '__main__':
unittest.main()
C:\Python27\python.exe D:/Moudle/module_1/test4.py
i am test1 the value of a is 1
.E
======================================================================
ERROR: test2 (__main__.Mydemo)
----------------------------------------------------------------------
Traceback (most recent call last):
File "D:/Moudle/module_1/test4.py", line 7, in test2
print "i am test2 the value of a is {}".format(self.a)
AttributeError: 'Mydemo' object has no attribute 'a'
----------------------------------------------------------------------
Ran 2 tests in 0.001s
FAILED (errors=1)
上面就是说明TestCase
类中所有的测试用例是独立的,每个testcase
就是由TestCase
实例化的一个独立的实例。那是不是就是每个TestCase
不能共享数据呢?答案是否定的,不能共享的原因是我们上面用到的是self(实例对象属性),能共享我们就必须使用类属性,比如下个例子:
import unittest
class Mydemo(unittest.TestCase):
def test1(self):
Mydemo.a=1
print "i am test1 the value of a is {}".format(self.a)
def test2(self):
print "i am test2 the value of a is {}".format(Mydemo.a)
if __name__ == '__main__':
unittest.main()
-----
运行结果如下:
i am test1 the value of a is 1
..
i am test2 the value of a is 1
----------------------------------------------------------------------
Ran 2 tests in 0.000s
OK
这些东西其实是python类的一些动态行为,但是既然和unnitest关联,就随便提下。我们运行test1的时候,给Mydemo加了一个新的属性a(值为1),当我们运行test2时,我们就能拿到Mydemo类的属性了。
说了TaseCase我们不得不说下TestSuite。TestSuite是有一个个TestCase组成的,当然TestSuite里面可以再嵌套TestSuite。我们打开C:\Python27\Lib\unittest\suite.py
找到TestSuite,它继承于BaseTestSuite,其实主要的一些属性就那么几个:
1.self._tests
这个私有变量里面方的是所有的TestCase
或者TestSuite
。
2.run()
方法,方法如下:
def run(self, result, debug=False):
topLevel = False
if getattr(result, '_testRunEntered', False) is False:
result._testRunEntered = topLevel = True
for test in self:#这个循环会一直遍历_tests中的变量
if result.shouldStop:
break
if _isnotsuite(test):
self._tearDownPreviousClass(test, result)
self._handleModuleFixture(test, result)
self._handleClassSetUp(test, result)#这一句提到了调用setUpClass的规则
result._previousTestClass = test.__class__
if (getattr(test.__class__, '_classSetupFailed', False) or
getattr(result, '_moduleSetUpFailed', False)):
continue
if not debug:
test(result)#如果是TestSuit继续调用该方法,如果是TestCase则调用TestCase中的run方法
else:
test.debug()
if topLevel:
self._tearDownPreviousClass(None, result)
self._handleModuleTearDown(result)
result._testRunEntered = False
return result
self是个迭代对象,一直遍历上文提到的self._tests
变量
我们看看_handleClassSetUp
中的方法,发现在在用例的执行过程中,每个TestCase
类只会调用一次setUpClass
方法,同理tearDownClass
。对用这一点我们举个例子:
import unittest
class Mydemo(unittest.TestCase):
@classmethod
def setUpClass(cls):
print "I am setUpClass"
def test1(self):
print "i am test1 "
def test2(self):
print "i am test2"
@classmethod
def tearDownClass(cls):
print "I am tearDownClass"
if __name__ == '__main__':
unittest.main()
C:\Python27\python.exe D:/Moudle/module_1/test4.py
I am setUpClass
..
i am test1
----------------------------------------------------------------------
i am test2
Ran 2 tests in 0.001s
I am tearDownClass
OK
说明类方法setUpClass
与tearDownClass
只执行了一遍了,这就回答了我们第一个问题了:在setUpClass
中启动浏览器,执行完所有流程后关闭浏览器,举一个简单的demo就是:
#coding=utf-8
import unittest
from selenium import webdriver
class Mydemo(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.browser=webdriver.Firefox()
def test1(self):
'''登录'''
browser=self.browser
#do someting about login
def test2(self):
'''查询'''
browser = self.browser
# do someting about search
def test3(self):
'''提交数据'''
browser = self.browser
# do someting about submmit
@classmethod
def tearDownClass(cls):
browser=self.browser
browser.close()
if __name__ == '__main__':
unittest.main()
上面就会在所有的case执行之前启动firefox,因为每个test中拿到的都是Mydemo类中同一个webdriver对象,所以能保证操作的都是同一个浏览器句柄。关于这个setUpClass如果想要动态的改变某个值一定要使用python的可变的对象比如list,dict等…这些其实都是一些python类的一些知识,算我啰嗦吧我还是想举个例子,嫌烦的同学,绕过这一部分吧。
#coding=utf-8
import unittest
from selenium import webdriver
class Mydemo(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.a=1
def test1(self):
print "before update the a in test1 is:{}".format(self.a)
self.a=self.a+1
print "after update the a in test1 is:{}".format(self.a)
def test2(self):
print "the value in test2 is:{}".format(self.a)
@classmethod
def tearDownClass(cls):
print "I am tearDownClass"
if __name__ == '__main__':
unittest.main()
运行结果:
C:\Python27\python.exe D:/Moudle/module_1/test4.py
before update the a in test1 is:1
..
after update the a in test1 is:2
----------------------------------------------------------------------
the value in test2 is:1
I am tearDownClass
Ran 2 tests in 0.001s
OK
我们想在test1中改变a的值,但是test2中的结果说明a没有被改变,这其实也很好理解。如果我们想要改变怎么办,看看下面的例子:
#coding=utf-8
import unittest
from selenium import webdriver
class Mydemo(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.a=[0]
def test1(self):
print "before update the a in test1 is:{}".format(self.a[0])
self.a[0]=self.a[0]+1
print "after update the a in test1 is:{}".format(self.a[0])
def test2(self):
print "the value in test2 is:{}".format(self.a[0])
@classmethod
def tearDownClass(cls):
print "I am tearDownClass"
if __name__ == '__main__':
unittest.main()
运行结果:
C:\Python27\python.exe D:/Moudle/module_1/test4.py
..
before update the a in test1 is:0
----------------------------------------------------------------------
after update the a in test1 is:1
Ran 2 tests in 0.000s
the value in test2 is:1
I am tearDownClass
OK
我们把a变成一个list,发现a的值在test2中改变了。好了这一部分就这样了。
这个其实也是python类的一些知识可能有的人没有关注就是__call__
这个魔法属性,我们看到在这个循环中test如果是testsuite对象,那么会调用中TestSuite类中的__call__
方法(在其父类BaseTestSuite中),该方法中会再次调用run方法。一直到test是个testcase对象,那么就会调用我们上文提到的TestCase中的__call__
(这就是我们上面提到为什么找有__call__
属性类实例的方法),一样该__call__
中的方法也是调用TestCase中的run。所以最终所有的执行其实都是执行TestCase中的run方法。
上面大致讲了一些TestCase与TestSuit的知识,可能穿插的比较多。