深入解读Python的unittest并拓展HTMLTestRunner
unnitest是Python的一個重要的單元測試框架,對于用Python進行開發的同事們可能不需要對他有過深入的了解會用就行,但是,對于自動化測試人員我覺得是要熟知unnitest的執行原理以及相關模塊的作用。我這邊提幾個簡單的需求如下:
1.如何利用unnitest執行流程測試而非單元測試。比如我們可能利用selenium+unnitest來跑一段流程,比如test1里面我們實現登陸,test2在test1成功登陸的基礎上,實現一個查詢的測試,test3我們查詢一些數據后,頁面選擇性提交數據。這個你總不能在test1里面登陸完然后關閉瀏覽器;在test2里面再登陸再執行查詢后關閉瀏覽器;test3再登陸執行提交數據。你或者可以這樣想把這幾個步驟寫在一個test里面。但是,如果流程過長怎么辦?自己難道感覺不到剪不斷理還亂的糾結嗎....
2.如何控制unnitest的執行順序。unnitest里面tests數組里面存放的TestCase默認是以首字母排序的,這對于test1,test2....test9這樣的執行順序是沒用問題的,但是對于多個test比如test1,test2.........test18,這樣unnitest可不是按照這個順序,我說了是按照首字母排序來的,他會這樣執行test1,test10,....test18,test2,..........test9,當然我說的這些對于單元測試是影響不大的(除非各個test之間有數據依賴關系,后面提到),對于流程可能是顛覆性的。
3.流程測試中如何動態的控制是否跳過某個test的執行。對于流程來說,這也是常見的一種想法,比如test1我連登陸都沒有成功,還有意義執行后面的test嗎,之后報出來的都是一些NosuchElement的錯誤,這些錯誤沒有任何意義,而且純粹浪費時間....出來的報告也是不"人性的"。那么我們現在一個好的想法是:如果test1沒有執行成功,后面的test能動態全部跳過,其實不止是test1沒成功,后面test跳過,準確的說是,test1,test2........testN中如果任意某個test沒有通過,后面的能動態的全部跳過。當然,如果后面的test和前面test沒什么關系,也可能選擇不管前面是否成功均不跳過;也可以是只和test1登錄有關,只要登錄成功了我就不跳過,如果登錄不成功我就跳過...還有很多.....更重要的是,報告中有所展現,不能說skip了某個用例,你報告就不顯示了,這樣老板認為你偷懶,用例寫這么少? 你還要瑟瑟發抖的去解釋,是因為前面的用例沒通過所以,沒顯示了...
我們高大上的是這樣的,test1執行失敗了,test2.......testN,報告中都體現,標注是skip的case,而且點開還有原因解釋:"test1沒有執行成功,所以跳過此case"。這就是我拓展HTMLTestRunner的原因,后面逐行解釋如果拓展它。
.......還有很多
補充一下,為什么往流程上扯呢,因為公司的模塊太多,而且復雜,單元測試機會不太會用,只要保證各個業務的主流程沒問題即可,但是不影響我們解析unnitest。
我們的想法很多,但是如何來實現呢?那就讓我們來深入探討下python的unnitest吧!
關于unnitest看似復雜我給出來就是unnitest=TestCase+TestResult,只要熟知這2個模塊,你就能"為所欲為"!!可能有人說不對不是有什么TestSuite嗎還有TextTestRunner等等嗎,不錯確實我們平時用到的大多是這些模塊,但是,到其實最終執行的是TestCase中的run方法,并把結果給TestResult(或它的子類)。我們先來看一個簡單的unnitest例子,并以此來拓展!例子如下:
import unittest class Mydemo(unittest.TestCase):def setUp(self):self.a=1def 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.000sOK這個是沒有問題的,那么我們可能要想這個unnitest.main()是什么東西,還有其他的寫法來執行嗎,能只執行test1,test2,不執行test3嗎(暫時不用skip)?那么我們從unittest.main()看起來。debug進入其實最終執行的是TestProgram這類,貼出構造函數部分代碼:
if argv is None:argv = sys.argv#得到當前模塊的絕對路徑self.exit = exitself.failfast = failfastself.catchbreak = catchbreakself.verbosity = verbosityself.buffer = bufferself.defaultTest = defaultTestself.testRunner = testRunnerself.testLoader = testLoaderself.progName = os.path.basename(argv[0]) self.parseArgs(argv)#查找當前module的Testsuiteself.runTests()#執行測試好了,從上面我們可以看出來其實也就2個主要的步驟就是第一:找出要測試的testcase,并加入到Testsuite,第二:運行Testsuite并把結果給TestResult。
首先,第一:了解什么是TestCase?什么是TestSuite?第二:如果找出這些Testcase,或者TestSuite?
什么是TestCase?
有人說TesetCase就是以test開頭的就叫一個testcase,我只能這樣說太偏面的,準確的說:是實例了一個TesetCase類的叫一個TestCase,比如這樣:
import unittest class Mydemo(unittest.TestCase):def setUp(self):self.a=1def 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 1OK?上面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開頭的為一個testcasetestFnNames = 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=1print "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 test2print "i am test2 the value of a is {}".format(self.a) AttributeError: 'Mydemo' object has no attribute 'a'---------------------------------------------------------------------- Ran 2 tests in 0.001sFAILED (errors=1)上面就是說明TestCase類中所有的測試用例是獨立的,每個testcase就是由TestCase實例化的一個獨立的實例。那是不是就是每個TestCase不能共享數據呢?答案是否定的,不能共享的原因是我們上面用到的是self(實例對象屬性),能共享我們就必須使用類屬性,比如下個例子:
import unittest class Mydemo(unittest.TestCase):def test1(self):Mydemo.a=1print "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.000sOK?這些東西其實是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 = Falseif getattr(result, '_testRunEntered', False) is False:result._testRunEntered = topLevel = Truefor test in self:#這個循環會一直遍歷_tests中的變量if result.shouldStop:breakif _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) orgetattr(result, '_moduleSetUpFailed', False)):continueif not debug:test(result)#如果是TestSuit繼續調用該方法,如果是TestCase則調用TestCase中的run方法else:test.debug()if topLevel:self._tearDownPreviousClass(None, result)self._handleModuleTearDown(result)result._testRunEntered = Falsereturn result?
?注釋1:self是個迭代對象,一直遍歷上文提到的self._tests變量
注釋2:我們看看_handleClassSetUp中的方法,發現在在用例的執行過程中,每個TestCase類只會調用一次setUpClass方法,同理tearDownClass。對用這一點我們舉個例子:
import unittest class Mydemo(unittest.TestCase):@classmethoddef setUpClass(cls):print "I am setUpClass"def test1(self):print "i am test1 "def test2(self):print "i am test2"@classmethoddef 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 tearDownClassOK說明類方法setUpClass與tearDownClass只執行了一遍了,這就回答了我們第一個問題了:在setUpClass中啟動瀏覽器,執行完所有流程后關閉瀏覽器,舉一個簡單的demo就是:
?
#coding=utf-8 import unittest from selenium import webdriver class Mydemo(unittest.TestCase):@classmethoddef setUpClass(cls):cls.browser=webdriver.Firefox()def test1(self):'''登錄'''browser=self.browser#do someting about logindef test2(self):'''查詢'''browser = self.browser# do someting about searchdef test3(self):'''提交數據'''browser = self.browser# do someting about submmit@classmethoddef tearDownClass(cls):browser=self.browserbrowser.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):@classmethoddef setUpClass(cls):cls.a=1def test1(self):print "before update the a in test1 is:{}".format(self.a)self.a=self.a+1print "after update the a in test1 is:{}".format(self.a)def test2(self):print "the value in test2 is:{}".format(self.a)@classmethoddef 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.001sOK?我們想在test1中改變a的值,但是test2中的結果說明a沒有被改變,這其實也很好理解。如果我們想要改變怎么辦,看看下面的例子:
#coding=utf-8 import unittest from selenium import webdriver class Mydemo(unittest.TestCase):@classmethoddef 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]+1print "after update the a in test1 is:{}".format(self.a[0])def test2(self):print "the value in test2 is:{}".format(self.a[0])@classmethoddef 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:1I 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的知識,可能穿插的比較多。
如何創建這些Testcase或者TestSuite?
1.自己手動實例化TestCase
這個上面已經有例子,與普通類無異,這中在自動化領域用處不大,我們不能一個個的實例化吧...
2.利用C:\Python27\Lib\unittest\loader.py模塊的TestLoader,該類提供了多種不同情境find testcase。
1.loadTestsFromTestCase利用給出的TestCase類名稱返回找到所有的suite。
2.loadTestsFromMoudle利用給出的Moudle返回找到所有的suite。
3.loadTestsFromName利用給出的Moudle名稱返回找到所有的suite。
4.discover返回給定目錄下符合pattern類型(默認test*.py)所有的suite。
其實這些方法最終都要歸結到loadTestsFromTestCase,可能官方不提供我們也能寫,既然有了就直接用吧。
經過上面的說明,我覺得大家對一TestCase,TestSuite應該有一個比較清楚的認識了,也解決了我自己的提問。問題一:我們可以用類方法setUpClass實現。對于問題二:我們可以利用TestLoader類中的方法返回suite,然后對這些suite按照自己的想法進行一些排序,然后再調用run方法。說完了TestCase我們再說下TestResult。
什么是TestResult?
顧名思義,testresult就是存儲測試結果的,不過通過何種方式調用run函數,最終到Testcase中的run方法時必須傳一個result(如果為None則自己實例化一個TestResult對象)。這個result就是TestResult對象或者是其子類的對象,我們每次執行的結果都會調用其addFailure,addSuccess,addSkip....等方法將執行結果保存到TestResult實例屬性中。我們還是來看看TestCase的run方法:
?通過注釋部分我們可以看出,每次執行用例時,都會把執行結果保存到TestResult中。我們再看看TextTestRunner這個類,在開始就使用了類TextTestResult,而這個類也是繼承TestResult,而后在執行的過程中最終把TextTestResult實例對象傳遞給TestCase的run方法。所以我上文說了,不過你是用什么方式執行unnitest,到最后都是TestCase的run方法與TestResult的游戲。而我們的HTMLTestRunner模塊也是在繼承在TestResult類的基礎上的。
說完了TestCase我們來看看第三個問題吧,也是比較有實際意義的話題,開始我是這樣跳過某些test的,代碼是這樣的:
?
#coding=utf-8 import unittest a=[False] class Mydemo(unittest.TestCase):def test1(self):try:print "i am test1"#test 1 do some thingexcept Exception,e:a[0] = Trueraise e@unittest.skipIf(a[0],"test1 fail skip test2")def test2(self):try:print "i am test2"raise AssertionError("error")# test2 do some thingexcept Exception,e:a[0] = Trueraise e@unittest.skipIf(a[0], "test1 fail skip test2")def test3(self):try:print "i am test3"# test2 do some thingexcept Exception, e:a[0] =Trueraise e if __name__ == '__main__':unittest.main()?
?想法很簡單:就是利用一個全局的數組,如果某個test執行出錯我就更改這個數組元素,到下一個case執行的時候就會判斷是否要跳過。上面因為test2出錯了,原本我們想跳過test3,但是很遺憾并沒有跳過test3!結果如下:
C:\Python27\python.exe D:/Moudle/module_1/test4.py i am test1 .F. i am test2 ====================================================================== i am test3 FAIL: test2 (__main__.Mydemo) ---------------------------------------------------------------------- Traceback (most recent call last):File "D:/Moudle/module_1/test4.py", line 20, in test2raise e AssertionError: error---------------------------------------------------------------------- Ran 3 tests in 0.000sFAILED (failures=1)原因很簡單:python在創建Mydemo這個類的時候,由于實例方法都使用了裝飾器unittest.skipIf,所以每個方法都向unittest.skipIf這個裝飾傳遞傳遞參數a[0],但是這個a[0]是沒用執行過任何case之前的a[0],也就是我們剛開始定義的a[0]=Flase,所以不可能跳過的。退一萬步講,即使這樣可行,也太不美觀了吧。我們想的是當執行當前的test時能判斷前面是否有出錯的case,有的話就跳過了。可行嗎?我覺得可行。主要就是用到我上面提到的TestCase中的_resultForDoCleanups的變量,這個其實就是TestResult一個引用。那么我們可以這樣寫:
#coding=utf-8 import unittest class Mydemo(unittest.TestCase):def test1(self):print "excute test1"def test2(self):if self._resultForDoCleanups.failures or self._resultForDoCleanups.errors:raise unittest.SkipTest("{} do not excute because {} is failed".format(self._testMethodName,self._resultForDoCleanups.failures[0][0]._testMethodName))print "excute test2"raise AssertionError("test2 fail")def test3(self):if self._resultForDoCleanups.failures or self._resultForDoCleanups.errors:raise unittest.SkipTest("{} do not excute because {} is failed".format(self._testMethodName,self._resultForDoCleanups.failures[0][0]._testMethodName))print "excute test3" if __name__ == '__main__':unittest.main()?運行結果如下:
.Fs ====================================================================== FAIL: test2 (__main__.Mydemo) ---------------------------------------------------------------------- Traceback (most recent call last):File "D:/Moudle/module_1/test4.py", line 10, in test2raise AssertionError("test2 fail") AssertionError: test2 fail---------------------------------------------------------------------- Ran 3 tests in 0.001sFAILED (failures=1, skipped=1) excute test1 excute test2?可以了,我們看出,test2失敗了,test3跳過;當然test2如果正確,test3會執行。目的是達到了,可是每個case都這樣寫不太好,我們想到了裝飾器(不會自行百度),在C:\Python27\Lib\unittest\case.py中新增如下代碼:
def Myskip(func):def RebackTest(self):if self._resultForDoCleanups.failures or self._resultForDoCleanups.errors:raise unittest.SkipTest("{} do not excute because {} is failed".format(func.__name__,self._resultForDoCleanups.failures[0][0]._testMethodName))func(self)return RebackTest?然后C:\Python27\Lib\unittest\__init__.py中新增:
__all__ = ['TestResult', 'TestCase', 'TestSuite','TextTestRunner', 'TestLoader', 'FunctionTestCase', 'main','defaultTestLoader', 'SkipTest', 'skip', 'skipIf', 'skipUnless','expectedFailure', 'TextTestResult', 'installHandler','registerResult', 'removeResult', 'removeHandler','Myskip'] ...... from .case import (TestCase, FunctionTestCase, SkipTest, skip, skipIf,Myskip,skipUnless, expectedFailure)?最終我們這樣寫:
#coding=utf-8 import unittest class Mydemo(unittest.TestCase):def test1(self):print "excute test1"@unittest.Myskipdef test2(self):print "excute test2"raise AssertionError("test2 fail")@unittest.Myskipdef test3(self):print "excute test3" if __name__ == '__main__':unittest.main()?好了,看上去還不錯....關于其他的unnitest相關知識,不想再扯了,最后拓展HTMLTestRunner報告,這可能是大家關心的!寫這個HTMLTestRunner的大神是在好久之前的了,基本能滿足大家需求。但是,目前對于web自動化,我覺得至少要新增2個東西。第一個新增skip列:因為我可能會skip某些case;第二新增截圖列,如果有錯誤我可能要截圖。
打了這么久字不想再多說了....我給出全部代碼,然后代碼中我改變的地方我給出標記并加注釋吧,完整代碼如下:(可能有點長,但是要有點耐心)
}
if (id.substr(0,2) == 'st') {
if (level > 1) {
tr.className = '';
}
else {
tr.className = 'hiddenRow';
}}} }function showClassDetail(cid, count) {var id_list = Array(count);var toHide = 1;for (var i = 0; i < count; i++) {tid0 = 't' + cid.substr(1) + '.' + (i+1);tid = 'f' + tid0;tr = document.getElementById(tid);if (!tr) {tid = 'p' + tid0;tr = document.getElementById(tid);} if (!tr) {tid = 's' + tid0;tr = document.getElementById(tid);}id_list[i] = tid;if (tr.className) {toHide = 0;}}for (var i = 0; i < count; i++) {tid = id_list[i];if (toHide) {document.getElementById('div_'+tid).style.display = 'none'document.getElementById(tid).className = 'hiddenRow';}else {document.getElementById(tid).className = '';}} }function showTestDetail(div_id){var details_div = document.getElementById(div_id)var displayState = details_div.style.display// alert(displayState)if (displayState != 'block' ) {displayState = 'block'details_div.style.display = 'block'}else {details_div.style.display = 'none'} }function html_escape(s) {s = s.replace(/&/g,'&');s = s.replace(/</g,'<');s = s.replace(/>/g,'>');return s; }/* obsoleted by detail in <div> function showOutput(id, name) {var w = window.open("", //urlname,"resizable,scrollbars,status,width=800,height=450");d = w.document;d.write("<pre>");d.write(html_escape(output_list[id]));d.write("\n");d.write("<a href='javascript:window.close()'>close</a>\n");d.write("</pre>\n");d.close(); } */ --></script>%(heading)s %(report)s %(ending)s</body> </html> """# variables: (title, generator, stylesheet, heading, report, ending)# ------------------------------------------------------------------------# Stylesheet## alternatively use a <link> for external style sheet, e.g.# <link rel="stylesheet" href="$url" type="text/css">STYLESHEET_TMPL = """ <style type="text/css" media="screen"> body { font-family: verdana, arial, helvetica, sans-serif; font-size: 80%; } table { font-size: 100%; } pre { word-wrap:break-word;word-break:break-all;overflow:auto;}/* -- heading ---------------------------------------------------------------------- */ h1 {font-size: 16pt;color: gray; } .heading {margin-top: 0ex;margin-bottom: 1ex; }.heading .attribute {margin-top: 1ex;margin-bottom: 0; }.heading .description {margin-top: 4ex;margin-bottom: 6ex; }/* -- css div popup ------------------------------------------------------------------------ */ a.popup_link { }a.popup_link:hover {color: red; }.popup_window {display: none;position: relative;left: 0px;top: 0px;/*border: solid #627173 1px; */padding: 10px;background-color: 00;font-family: "Lucida Console", "Courier New", Courier, monospace;text-align: left;font-size: 8pt;width: 600px; }} /* -- report ------------------------------------------------------------------------ */ #show_detail_line {margin-top: 3ex;margin-bottom: 1ex; } #result_table {width: 80%;border-collapse: collapse;border: 1px solid #777; } #header_row {font-weight: bold;color: white;background-color: #777; } #result_table td {border: 1px solid #777;padding: 2px; } #total_row { font-weight: bold; } .passClass { background-color: #6c6; } .failClass { background-color: #c60; } .errorClass { background-color: #c00; } .passCase { color: #6c6; } .failCase { color: #c60; font-weight: bold; } .errorCase { color: #c00; font-weight: bold; } .hiddenRow { display: none; } .testcase { margin-left: 2em; }/* -- ending ---------------------------------------------------------------------- */ #ending { }</style> """# ------------------------------------------------------------------------# Heading#HEADING_TMPL = """<div class='heading'> <h1>%(title)s</h1> %(parameters)s <p class='description'>%(description)s</p> </div>""" # variables: (title, parameters, description)HEADING_ATTRIBUTE_TMPL = """<p class='attribute'><strong>%(name)s:</strong> %(value)s</p> """ # variables: (name, value)# ------------------------------------------------------------------------# Report#REPORT_TMPL = """ <p id='show_detail_line'>Show <a href='javascript:showCase(0)'>Summary</a> <a href='javascript:showCase(1)'>Failed</a> <a href='javascript:showCase(2)'>All</a> </p> <table id='result_table'> <colgroup> <col align='left' /> <col align='right' /> <col align='right' /> <col align='right' /> <col align='right' /> <col align='right' /> </colgroup> <tr id='header_row'><td>Test Group/Test case</td><td>Count</td><td>Pass</td><td>Fail</td><td>Error</td><td>Skip</td><td>View</td><td>Screenshot</td> </tr> %(test_list)s <tr id='total_row'><td>Total</td><td>%(count)s</td><td>%(Pass)s</td><td>%(fail)s</td><td>%(error)s</td><td>%(skip)s</td><td>?</td><td>?</td></tr> </table> """ # variables: (test_list, count, Pass, fail, error)REPORT_CLASS_TMPL = r""" <tr class='%(style)s'><td>%(desc)s</td><td>%(count)s</td><td>%(Pass)s</td><td>%(fail)s</td><td>%(error)s</td> <td>%(skip)s</td><td><a href="javascript:showClassDetail('%(cid)s',%(count)s)">Detail</a></td><td>?</td> </tr> """ # variables: (style, desc, count, Pass, fail,skip, error, cid)REPORT_TEST_WITH_OUTPUT_TMPL = r""" <tr id='%(tid)s' class='%(Class)s'><td class='%(style)s'><div class='testcase'>%(desc)s</div></td><td colspan='6' align='center'><!--css div popup start--><a class="popup_link" οnfοcus='this.blur();' href="javascript:showTestDetail('div_%(tid)s')" >%(status)s</a><div id='div_%(tid)s' class="popup_window" ><div style='text-align: right; color:red;cursor:pointer'><a οnfοcus='this.blur();' οnclick="document.getElementById('div_%(tid)s').style.display = 'none' " >[x]</a></div><pre>%(script)s</pre></div><!--css div popup end--></td><td align='center'><a %(hidde)s href="%(image)s">picture_shot</a></td> </tr> """ # variables: (tid, Class, style, desc, status) REPORT_TEST_NO_OUTPUT_TMPL = r""" <tr id='%(tid)s' class='%(Class)s'><td class='%(style)s'><div class='testcase'>%(desc)s</div></td><td colspan='6' align='center'>%(status)s</td><td align='center'><a %(hidde)s href="%(image)s">picture_shot</a></td> </tr> """ # variables: (tid, Class, style, desc, status)REPORT_TEST_OUTPUT_TMPL = r""" %(id)s: %(output)s """# variables: (id, output)# ------------------------------------------------------------------------# ENDING#ENDING_TMPL = """<div id='ending'>?</div>"""# -------------------- The end of the Template class -------------------TestResult = unittest.TestResultclass _TestResult(TestResult):# note: _TestResult is a pure representation of results.# It lacks the output and reporting ability compares to unittest._TextTestResult.def __init__(self, verbosity=1):TestResult.__init__(self)self.stdout0 = Noneself.stderr0 = Noneself.success_count = 0self.skipped_count=0#add skipped_countself.failure_count = 0self.error_count = 0self.verbosity = verbosity# result is a list of result in 4 tuple# (# result code (0: success; 1: fail; 2: error),# TestCase object,# Test output (byte string),# stack trace,# )self.result = []def startTest(self, test):TestResult.startTest(self, test)# just one buffer for both stdout and stderrself.outputBuffer = io.BytesIO()stdout_redirector.fp = self.outputBufferstderr_redirector.fp = self.outputBufferself.stdout0 = sys.stdoutself.stderr0 = sys.stderrsys.stdout = stdout_redirectorsys.stderr = stderr_redirectordef complete_output(self):"""Disconnect output redirection and return buffer.Safe to call multiple times."""if self.stdout0:sys.stdout = self.stdout0sys.stderr = self.stderr0self.stdout0 = Noneself.stderr0 = Nonereturn self.outputBuffer.getvalue()def stopTest(self, test):# Usually one of addSuccess, addError or addFailure would have been called.# But there are some path in unittest that would bypass this.# We must disconnect stdout in stopTest(), which is guaranteed to be called.self.complete_output()def addSuccess(self, test):self.success_count += 1TestResult.addSuccess(self, test)output = self.complete_output()self.result.append((0, test, output, ''))if self.verbosity > 1:sys.stderr.write('ok ')sys.stderr.write(str(test))sys.stderr.write('\n')else:sys.stderr.write('.') def addSkip(self, test, reason):self.skipped_count+= 1TestResult.addSkip(self, test,reason)output = self.complete_output()self.result.append((3, test,'',reason))if self.verbosity > 1:sys.stderr.write('skip ')sys.stderr.write(str(test))sys.stderr.write('\n')else:sys.stderr.write('s')def addError(self, test, err):self.error_count += 1TestResult.addError(self, test, err)_, _exc_str = self.errors[-1]output = self.complete_output()self.result.append((2, test, output, _exc_str))if self.verbosity > 1:sys.stderr.write('E ')sys.stderr.write(str(test))sys.stderr.write('\n')else:sys.stderr.write('E')def addFailure(self, test, err):self.failure_count += 1TestResult.addFailure(self, test, err)_, _exc_str = self.failures[-1]output = self.complete_output()self.result.append((1, test, output, _exc_str))if self.verbosity > 1:sys.stderr.write('F ')sys.stderr.write(str(test))sys.stderr.write('\n')else:sys.stderr.write('F')class HTMLTestRunner(Template_mixin):""""""def __init__(self, stream=sys.stdout, verbosity=1, title=None, description=None,name=None):self.stream = streamself.verbosity = verbosityif title is None:self.title = self.DEFAULT_TITLEelse:self.title = titleif name is None:self.name =''else:self.name = nameif description is None:self.description = self.DEFAULT_DESCRIPTIONelse:self.description = descriptionself.startTime = datetime.datetime.now()def run(self, test):"Run the given test case or test suite."result = _TestResult(self.verbosity)test(result)self.stopTime = datetime.datetime.now()self.generateReport(test, result)# print (sys.stderr, '\nTime Elapsed: %s' % (self.stopTime-self.startTime))return resultdef sortResult(self, result_list):# unittest does not seems to run in any particular order.# Here at least we want to group them together by class.rmap = {}classes = []for n,t,o,e in result_list:cls = t.__class__if not cls in rmap:rmap[cls] = []classes.append(cls)rmap[cls].append((n,t,o,e))r = [(cls, rmap[cls]) for cls in classes]return rdef getReportAttributes(self, result):"""Return report attributes as a list of (name, value).Override this to add custom attributes."""startTime = str(self.startTime)[:19]duration = str(self.stopTime - self.startTime)status = []if result.success_count: status.append('Pass %s' % result.success_count)if result.failure_count: status.append('Failure %s' % result.failure_count) if result.skipped_count: status.append('Skip %s' % result.skipped_count)if result.error_count: status.append('Error %s' % result.error_count )if status:status = ' '.join(status)else:status = 'none'return [('Start Time', startTime),('Duration', duration),('Status', status),]def generateReport(self, test, result):report_attrs = self.getReportAttributes(result)#報告的頭部generator = 'HTMLTestRunner %s' % __version__stylesheet = self._generate_stylesheet()#拿到css文件heading = self._generate_heading(report_attrs)report = self._generate_report(result)ending = self._generate_ending()output = self.HTML_TMPL % dict(title = saxutils.escape(self.title),generator = generator,stylesheet = stylesheet,heading = heading,report = report,ending = ending,)self.stream.write(output.encode('utf8'))def _generate_stylesheet(self):return self.STYLESHEET_TMPLdef _generate_heading(self, report_attrs):a_lines = []for name, value in report_attrs:line = self.HEADING_ATTRIBUTE_TMPL % dict(name = saxutils.escape(name),value = saxutils.escape(value),)a_lines.append(line)heading = self.HEADING_TMPL % dict(title = saxutils.escape(self.title),parameters = ''.join(a_lines),description = saxutils.escape(self.description),)return heading #根據result收集報告def _generate_report(self, result):rows = []sortedResult = self.sortResult(result.result)i = 0for cid, (cls, cls_results) in enumerate(sortedResult):# subtotal for a class np = nf =ns=ne = 0#np代表pass個數,nf代表fail,ns代表skip,ne,代表errorfor n,t,o,e in cls_results:if n == 0: np += 1elif n == 1: nf += 1 elif n==3:ns+=1else: ne += 1# format class description# if cls.__module__ == "__main__":# name = cls.__name__# else:# name = "%s.%s" % (cls.__module__, cls.__name__)name = cls.__name__try:core_name=self.name[i]except Exception,e:core_name =''# doc = (cls.__doc__)+core_name and (cls.__doc__+core_name).split("\n")[0] or ""doc = (cls.__doc__) and cls.__doc__ .split("\n")[0] or ""desc = doc and '%s: %s' % (name, doc) or namei=i+1
#生成每個TestCase類的匯總數據,對于報告中的row = self.REPORT_CLASS_TMPL % dict(style = ne > 0 and 'errorClass' or nf > 0 and 'failClass' or 'passClass',desc = desc,count = np+nf+ne+ns,Pass = np,fail = nf,error = ne, skip=ns,cid = 'c%s' % (cid+1),)rows.append(row)#生成每個TestCase類中所有方法的測試結果for tid, (n,t,o,e) in enumerate(cls_results):self._generate_report_test(rows, cid, tid, n, t, o, e)report = self.REPORT_TMPL % dict(test_list = ''.join(rows),count = str(result.success_count+result.failure_count+result.error_count+result.skipped_count),Pass = str(result.success_count),fail = str(result.failure_count),error = str(result.error_count), skip=str(result.skipped_count))return reportdef _generate_report_test(self, rows, cid, tid, n, t, o, e):# e.g. 'pt1.1', 'ft1.1', etchas_output = bool(o or e) tid = (n == 0 and 'p' or n==3 and 's' or 'f') + 't%s.%s' % (cid+1,tid+1)name = t.id().split('.')[-1]doc = t.shortDescription() or ""desc = doc and ('%s: %s' % (name, doc)) or nametmpl = has_output and self.REPORT_TEST_WITH_OUTPUT_TMPL or self.REPORT_TEST_NO_OUTPUT_TMPLuo1=""# o and e should be byte string because they are collected from stdout and stderr?if isinstance(o,str):uo = str(o)else:uo = eif isinstance(e,str):# TODO: some problem with 'string_escape': it escape \n and mess up formating# ue = unicode(e.encode('string_escape'))ue = eelse:ue = oscript = self.REPORT_TEST_OUTPUT_TMPL % dict(id = tid,output = saxutils.escape(str(uo) + str(ue)))if "shot_picture_name" in str(saxutils.escape(str(ue))):hidde_status=''pattern = re.compile(r'AssertionError:.*?shot_picture_name=(.*)',re.S)shot_name =re.search(pattern,str(saxutils.escape(str(e))))try:image_url="http://192.168.99.105/contractreport/screenshot/"+time.strftime("%Y-%m-%d", time.localtime(time.time()))+"/"+shot_name.group(1)+".png"except Exception,e:image_url = "http://192.168.99.105/contractreport/screenshot/" + time.strftime("%Y-%m-%d",time.localtime(time.time()))else:hidde_status = '''hidden="hidden"'''image_url=''row = tmpl % dict(tid = tid,Class = (n == 0 and 'hiddenRow' or 'none'), style=n == 2 and 'errorCase' or (n == 1 and 'failCase') or (n == 3 and 'skipCase' or 'none'),desc = desc,script = script,hidde=hidde_status,image=image_url,status = self.STATUS[n],)rows.append(row)if not has_output:returndef _generate_ending(self):return self.ENDING_TMPL############################################################################## # Facilities for running tests from the command line ############################################################################### Note: Reuse unittest.TestProgram to launch test. In the future we may # build our own launcher to support more specific command line # parameters like test title, CSS, etc. # class TestProgram(unittest.TestProgram): # """ # A variation of the unittest.TestProgram. Please refer to the base # class for command line parameters. # """ # def runTests(self): # # Pick HTMLTestRunner as the default test runner. # # base class's testRunner parameter is not useful because it means # # we have to instantiate HTMLTestRunner before we know self.verbosity. # if self.testRunner is None: # self.testRunner = HTMLTestRunner(verbosity=self.verbosity) # unittest.TestProgram.runTests(self) # # main = TestProgram############################################################################## # Executing this module from the command line ##############################################################################if __name__ == "__main__":main(module=None)
?把上面代碼復制覆蓋原來的HTMLTestRunner就好,截圖那塊我是把錯誤的圖像放在apache服務器的某個路徑下的,如果有錯誤就顯示圖片超鏈接,沒有就隱藏這超鏈接。
關于上面的改動其實很簡單,熟悉一定的前端語言(html.javascript)即可。HTMLTestRunner原理就是我們上文提到的利用_TestResult繼承unnitest中的TestResult類,并重寫了addSuccess,addSkip,addError等方法,把測試結果放在一個self.result里面,最后遍歷這個result利用前端的一些知識生成一個html報告。這邊貼圖貼一下生成的樣式吧,執行testcase的代碼:
#coding=utf-8 import unittest import HTMLTestRunner import sys,os class Mydemo(unittest.TestCase):def test1(self):print "excute test1"@unittest.Myskipdef test2(self):print "excute test2"raise AssertionError("test2 fail")@unittest.Myskipdef test3(self):print "excute test3"@unittest.Myskipdef test4(self):print "excute test4" if __name__ == '__main__':module_name=os.path.basename(sys.argv[0]).split(".")[0]module=__import__(module_name)fp=file("./new.html","wb")runner=HTMLTestRunner.HTMLTestRunner(fp)all_suite=unittest.defaultTestLoader.loadTestsFromModule(module)runner.run(all_suite)最后生成的報告如下:說了這么多只是希望大家能對unnitest有更多的了解,當然如果你已經懂的更多或者認為我某些地方說錯了,請一笑而過....
?
轉載于:https://www.cnblogs.com/hhudaqiang/p/6596043.html
總結
以上是生活随笔為你收集整理的深入解读Python的unittest并拓展HTMLTestRunner的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 查找文件命令find总结以及查找大文件
- 下一篇: 06:整数奇偶排序