深入解讀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=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?

什么是TestCase?

有人說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

 

 注釋1:self是個迭代對象,一直遍歷上文提到的self._tests變量

注釋2:我們看看_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的知識,可能穿插的比較多。

如何創建這些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方法:

def run(self, result=None):
        orig_result = result
        if result is None:#如果沒有傳入result對象自己實例化一個TestResult對象
            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)
        if (getattr(self.__class__, "__unittest_skip__", False) or
            getattr(testMethod, "__unittest_skip__", False)):
            # If the class or method was skipped.
            try:
                skip_why = (getattr(self.__class__, '__unittest_skip_why__', '')
                            or getattr(testMethod, '__unittest_skip_why__', ''))
                self._addSkip(result, skip_why)#調用addSkip
            finally:
                result.stopTest(self)
            return
        try:
            success = False
            try:
                self.setUp()
            except SkipTest as e:
                self._addSkip(result, str(e))
            except KeyboardInterrupt:
                raise
            except:
                result.addError(self, sys.exc_info())#調用addError
            else:
                try:
                    testMethod()
                except KeyboardInterrupt:
                    raise
                except self.failureException:
                    result.addFailure(self, sys.exc_info())#調用addFailure
                except _ExpectedFailure as e:
                    addExpectedFailure = getattr(result, 'addExpectedFailure', None)
                    if addExpectedFailure is not None:
                        addExpectedFailure(self, e.exc_info)#調用addExpectedFailure
                    else:
                        warnings.warn("TestResult has no addExpectedFailure method, reporting as passes",
                                      RuntimeWarning)
                        result.addSuccess(self)#調用addSuccess
                except _UnexpectedSuccess:
                    addUnexpectedSuccess = getattr(result, 'addUnexpectedSuccess', None)
                    if addUnexpectedSuccess is not None:
                        addUnexpectedSuccess(self)
                    else:
                        warnings.warn("TestResult has no addUnexpectedSuccess method, reporting as failures",
                                      RuntimeWarning)
                        result.addFailure(self, sys.exc_info())
                except SkipTest as e:
                    self._addSkip(result, str(e))
                except:
                    result.addError(self, sys.exc_info())
                else:
                    success = True

                try:
                    self.tearDown()
                except KeyboardInterrupt:
                    raise
                except:
                    result.addError(self, sys.exc_info())
                    success = False

            cleanUpSuccess = self.doCleanups()
            success = success and cleanUpSuccess
            if success:
                result.addSuccess(self)
        finally:
            result.stopTest(self)
            if orig_result is None:
                stopTestRun = getattr(result, 'stopTestRun', None)
                if stopTestRun is not None:
                    stopTestRun()

 通過注釋部分我們可以看出,每次執行用例時,都會把執行結果保存到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 thing
        except Exception,e:
            a[0] = True
            raise e
    @unittest.skipIf(a[0],"test1 fail skip test2")
    def test2(self):
        try:
            print "i am test2"
            raise  AssertionError("error")
            # test2 do some thing
        except Exception,e:
            a[0] = True
            raise e
    @unittest.skipIf(a[0], "test1 fail skip test2")
    def test3(self):
        try:
            print "i am test3"
            # test2 do some thing
        except Exception, e:
            a[0] =True
            raise 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 test2
    raise e
AssertionError: error

----------------------------------------------------------------------
Ran 3 tests in 0.000s

FAILED (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 test2
    raise AssertionError("test2 fail")
AssertionError: test2 fail

----------------------------------------------------------------------
Ran 3 tests in 0.001s

FAILED (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.Myskip
    def test2(self):
        print "excute test2"
        raise AssertionError("test2 fail")
    @unittest.Myskip
    def test3(self):
        print "excute test3"
if __name__ == '__main__':
    unittest.main()

 好了,看上去還不錯....關於其他的unnitest相關知識,不想再扯了,最后拓展HTMLTestRunner報告,這可能是大家關心的!寫這個HTMLTestRunner的大神是在好久之前的了,基本能滿足大家需求。但是,目前對於web自動化,我覺得至少要新增2個東西。第一個新增skip列:因為我可能會skip某些case;第二新增截圖列,如果有錯誤我可能要截圖。
打了這么久字不想再多說了....我給出全部代碼,然后代碼中我改變的地方我給出標記並加注釋吧,完整代碼如下:(可能有點長,但是要有點耐心)

#coding=utf-8
"""
A TestRunner for use with the Python unit testing framework. It
generates a HTML report to show the result at a glance.

The simplest way to use this is to invoke its main method. E.g.

    import unittest
    import HTMLTestRunner

    ... define your tests ...

    if __name__ == '__main__':
        HTMLTestRunner.main()


For more customization options, instantiates a HTMLTestRunner object.
HTMLTestRunner is a counterpart to unittest's TextTestRunner. E.g.

    # output to a file
    fp = file('my_report.html', 'wb')
    runner = HTMLTestRunner.HTMLTestRunner(
                stream=fp,
                title='My unit test',
                description='This demonstrates the report output by HTMLTestRunner.'
                )

    # Use an external stylesheet.
    # See the Template_mixin class for more customizable options
    runner.STYLESHEET_TMPL = '<link rel="stylesheet" href="my_stylesheet.css" type="text/css">'

    # run the test
    runner.run(my_test_suite)


------------------------------------------------------------------------
Copyright (c) 2004-2007, Wai Yip Tung
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:

* Redistributions of source code must retain the above copyright notice,
  this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
  notice, this list of conditions and the following disclaimer in the
  documentation and/or other materials provided with the distribution.
* Neither the name Wai Yip Tung nor the names of its contributors may be
  used to endorse or promote products derived from this software without
  specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""

# URL: http://tungwaiyip.info/software/HTMLTestRunner.html

__author__ = "Wai Yip Tung"
__version__ = "0.8.2"


"""
Change History

Version 0.8.2
* Show output inline instead of popup window (Viorel Lupu).

Version in 0.8.1
* Validated XHTML (Wolfgang Borgert).
* Added description of test classes and test cases.

Version in 0.8.0
* Define Template_mixin class for customization.
* Workaround a IE 6 bug that it does not treat <script> block as CDATA.

Version in 0.7.1
* Back port to Python 2.3 (Frank Horowitz).
* Fix missing scroll bars in detail log (Podi).
"""

# TODO: color stderr
# TODO: simplify javascript using ,ore than 1 class in the class attribute?
#coding=utf-8
import datetime
import io
import sys
reload(sys)
sys.setdefaultencoding('utf8')
import time
import unittest
import re
from xml.sax import saxutils


# ------------------------------------------------------------------------
# The redirectors below are used to capture output during testing. Output
# sent to sys.stdout and sys.stderr are automatically captured. However
# in some cases sys.stdout is already cached before HTMLTestRunner is
# invoked (e.g. calling logging.basicConfig). In order to capture those
# output, use the redirectors for the cached stream.
#
# e.g.
#   >>> logging.basicConfig(stream=HTMLTestRunner.stdout_redirector)
#   >>>

class OutputRedirector(object):
    """ Wrapper to redirect stdout or stderr """
    def __init__(self, fp):
        self.fp = fp

    def write(self, s):
        self.fp.write(s)

    def writelines(self, lines):
        self.fp.writelines(lines)

    def flush(self):
        self.fp.flush()

stdout_redirector = OutputRedirector(sys.stdout)
stderr_redirector = OutputRedirector(sys.stderr)



# ----------------------------------------------------------------------
# Template

class Template_mixin(object):
    """
    Define a HTML template for report customerization and generation.

    Overall structure of an HTML report

    HTML
    +------------------------+
    |<html>                  |
    |  <head>                |
    |                        |
    |   STYLESHEET           |
    |   +----------------+   |
    |   |                |   |
    |   +----------------+   |
    |                        |
    |  </head>               |
    |                        |
    |  <body>                |
    |                        |
    |   HEADING              |
    |   +----------------+   |
    |   |                |   |
    |   +----------------+   |
    |                        |
    |   REPORT               |
    |   +----------------+   |
    |   |                |   |
    |   +----------------+   |
    |                        |
    |   ENDING               |
    |   +----------------+   |
    |   |                |   |
    |   +----------------+   |
    |                        |
    |  </body>               |
    |</html>                 |
    +------------------------+
    """

    STATUS = {
    0: 'pass',
    1: 'fail',
    2: 'error',
    3:'skip'
    }

    DEFAULT_TITLE = 'Unit Test Report'
    DEFAULT_DESCRIPTION = ''

    # ------------------------------------------------------------------------
    # HTML Template

    HTML_TMPL = r"""<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title>%(title)s</title>
    <meta name="generator" content="%(generator)s"/>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
    %(stylesheet)s
</head>
<body>
<script language="javascript" type="text/javascript"><!--
output_list = Array();

/* level - 0:Summary; 1:Failed; 2:All */
function showCase(level) {
    trs = document.getElementsByTagName("tr");
    for (var i = 0; i < trs.length; i++) {
        tr = trs[i];
        id = tr.id;
        if (id.substr(0,2) == 'ft') {
            if (level < 1) {
                tr.className = 'hiddenRow';
            }
            else {
                tr.className = '';
            }
        }
        if (id.substr(0,2) == 'pt') {
            if (level > 1) {
                tr.className = '';
            }
            else {
                tr.className = 'hiddenRow';
            }
}
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("", //url name, "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" onfocus='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 onfocus='this.blur();' onclick="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.TestResult class _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 = None self.stderr0 = None self.success_count = 0 self.skipped_count=0#add skipped_count self.failure_count = 0 self.error_count = 0 self.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 stderr self.outputBuffer = io.BytesIO() stdout_redirector.fp = self.outputBuffer stderr_redirector.fp = self.outputBuffer self.stdout0 = sys.stdout self.stderr0 = sys.stderr sys.stdout = stdout_redirector sys.stderr = stderr_redirector def complete_output(self): """ Disconnect output redirection and return buffer. Safe to call multiple times. """ if self.stdout0: sys.stdout = self.stdout0 sys.stderr = self.stderr0 self.stdout0 = None self.stderr0 = None return 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 += 1 TestResult.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+= 1 TestResult.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 += 1 TestResult.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 += 1 TestResult.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 = stream self.verbosity = verbosity if title is None: self.title = self.DEFAULT_TITLE else: self.title = title if name is None: self.name ='' else: self.name = name if description is None: self.description = self.DEFAULT_DESCRIPTION else: self.description = description self.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 result def 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 r def 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_TMPL def _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 = 0 for cid, (cls, cls_results) in enumerate(sortedResult): # subtotal for a class np = nf =ns=ne = 0#np代表pass個數,nf代表fail,ns代表skip,ne,代表error for n,t,o,e in cls_results: if n == 0: np += 1 elif n == 1: nf += 1 elif n==3:ns+=1 else: 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 name i=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 report def _generate_report_test(self, rows, cid, tid, n, t, o, e): # e.g. 'pt1.1', 'ft1.1', etc has_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 name tmpl = has_output and self.REPORT_TEST_WITH_OUTPUT_TMPL or self.REPORT_TEST_NO_OUTPUT_TMPL uo1="" # o and e should be byte string because they are collected from stdout and stderr? if isinstance(o,str): uo = str(o) else: uo = e if isinstance(e,str): # TODO: some problem with 'string_escape': it escape \n and mess up formating # ue = unicode(e.encode('string_escape')) ue = e else: ue = o script = 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: return def _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類,並重寫了addSuccessaddSkip,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.Myskip
    def test2(self):
        print "excute test2"
        raise AssertionError("test2 fail")
    @unittest.Myskip
    def test3(self):
        print "excute test3"
    @unittest.Myskip
    def 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有更多的了解,當然如果你已經懂的更多或者認為我某些地方說錯了,請一笑而過....

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM