python unittest架構

來源:互聯網
上載者:User

標籤:python unittest 單元 測試

    unittest模組提供了單元測試的組件,方便開發人員進行自測。

    一、unittest中的重要概念:

    測試案例:測試案例對象是最小的測試單位,針對指定的輸入來測試期待的輸出。由類TestCase的衍生類別或FunctionTestCase類來建立的。

    測試韌體:代表了測試相關的準備和清除工作,比如在一個測試進行之前需要建立資料庫連接,測試結束之後需要關閉資料庫連接。測試韌體是在TestCase子類中進行重載的setUp和tearDown函數實現的。每個測試案例執行前後都會自動執行setUp和tearDown方法。另外如果setUp執行拋出異常,則忽略未執行的測試案例,測試結束

    測試套件:包含一組測試案例,一起執行。同時,也可以包含其他測試套件。可以通過TestSuite類建立對象來添加測試案例;也可以使用unittest提供的TestLoader來自動將指定的測試案例收集到一個自動建立的TestSuit對象中。

    測試驅動:主要負責執行測試,並反饋測試結果。TestRunner對象存在一個run()方法,它接收一個TestCase對象或TestSuit對象作為參數,返回測試的結果對象(TestResult)

    

    二、編寫最簡單的測試代碼

    下面是一個數學操作的類,包含加法和除法操作。並提供了對應的單元測試代碼,從這個例子上,我們學習一些unittest基本的功能:

#exam.py檔案提供了供測試的樣本類#coding: utf-8class operator(object):    def __init__(self, a, b):        self.a = a         self.b = b         def add(self):        return self.a + self.b         def divide(self):        return self.a / self.b     #test.py檔案提供了通過unittest構建的測試代碼    #coding:utf-8from exam import operatorimport unittestclass TestOperator(unittest.TestCase):        def setUp(self):               #test fixture        self.oper = operator(10,0)    def test_add(self):            #test case        self.assertEqual(self.oper.add(), 10, u"加法基礎功能不滿足要求")    def test_divide(self):        self.assertRaises(ZeroDivisionError, self.oper.divide())        #def tearDown(self):        #pass        if __name__ == "__main__":    unittest.main(verbosity=2)

運行test.py檔案,即可見到下面的輸出:

test_add (__main__.TestOperator) ... oktest_divide (__main__.TestOperator) ... ok----------------------------------------------------------------------Ran 2 tests in 0.000sOK
  • 測試類別需要繼承自TestCase

  • 測試方法預設是通過首碼test來標示的,所以在測試類別中添加非test首碼的輔助方法並不會影響測試案例的搜集。

  • 測試方法一般通過TestCase提供的assert*方法來判斷結果是否符合預期。

  • 每個測試執行個體都僅包含一個test*方法,即上面的代碼會建立兩個測試執行個體,每個測試執行個體包含一個test*的方法

  • unittest.main提供了命令列的介面,啟動測試,並反饋測試結果。其中的參數verbosity指詳細顯示測試結果。

    想象:main中的邏輯應該是挺複雜的,需要構建test執行個體對象?需要找到那些是用於測試的方法?需要統計測試結果?等等一些我們還沒認識到的東西?

    解決這些困惑的方法很直接,讓我們調試main函數吧,,come on!

    我們可以看到main代表一個命令列介面類:我們可以通過命令列的方式執行測試,這和通過代碼中的main啟動測試時一樣的過程。

main = TestProgram                            #...class TestProgram(object):                    #命令列介面類    """A command-line program that runs a set of tests; this is primarily       for making test modules conveniently executable.    """

    運行main(),即無傳參調用__init__.py來構建一個對象。

def __init__(self, module=‘__main__‘, defaultTest=None, argv=None,                    testRunner=None, testLoader=loader.defaultTestLoader,                    exit=True, verbosity=1, failfast=None, catchbreak=None,                    buffer=None):    。。。。    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)                       #解析參數argv,並載入test    self.runTests()                            #運行test,並反饋結果

    在執行__init__.py的過程中,首先進行一些初始化工作,即傳入main的參數或是通過命令列添加的參數影響了unittest內部的某些特性,比如例子中的verbosity代表了測試結果輸出的詳細度,如果被設定為1,或者不設定,結果中將不會顯示具體的testcase名稱,大家可以自己驗證一下;

    接下來,進入self.parseArgs(argv),讓我們看下它做了什麼:

def parseArgs(self, argv):        if len(argv) > 1 and argv[1].lower() == ‘discover‘:            self._do_discovery(argv[2:])            return        。。。。        try:            options, args = getopt.getopt(argv[1:], ‘hHvqfcb‘, long_opts)            for opt, value in options:                if opt in (‘-h‘,‘-H‘,‘--help‘):                    self.usageExit()                if opt in (‘-q‘,‘--quiet‘):                    self.verbosity = 0                if opt in (‘-v‘,‘--verbose‘):    #命令列參數-v即代表了main參數verbosity                    self.verbosity = 2                if opt in (‘-f‘,‘--failfast‘):                    if self.failfast is None:                        self.failfast = True            。。。。                     #以上是從argv中讀取參數,並適當對初始化值進行修改            self.createTests()          #建立測試執行個體,返回他們的集合-suit對象(測試套件)                          。。。。

    首先,參數如果是‘discover’則進入另一個分支,是關於自動探索的功能,後面會講到。

    然後開始解析argv,這裡的argv首選傳入main的argv參數,如果為None,則取命令列執行該指令碼時傳遞的sys.argv。可以看到命令列傳遞的sys.argv參數和傳遞到main的其他參數是相互替代的,這就達到了通過命令列傳參啟動和通過main代碼傳參啟動,效果是一樣的。

    接下來調用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)

    僅從方法的名字就可以看出,建立Tests就是在模組或是具體的test方法上載入。載入的過程主要就是搜集測試方法,建立TestCase執行個體,並返回包含有這些case的TestSuit對象,後面會詳細看下。

    至此,建立測試執行個體完成,接著就回到__init__中執行self.runTest()來真正啟動測試了:

def runTests(self):        if self.catchbreak:          #-c表示運行過程中捕捉CTRL+C異常            installHandler()        if self.testRunner is None:              self.testRunner = runner.TextTestRunner      #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         #以上部分是構建testRunner對象,即測試驅動        self.result = testRunner.run(self.test)  #就像上面講到的由runner的run方法啟動測試        if self.exit:            sys.exit(not self.result.wasSuccessful())

    從代碼中可以看出,測試由testRunner執行個體通過run函數來啟動,預設的testRunner是unittest提供的TextTestRunner。這個run方法設計很亮眼,感興趣的同志可以深入看下,裡面涉及了__call__和__iter__的用法並且巧妙結合。

    main函數簡單的調用即代替我們完成了基本的測試功能,其內部可是複雜滴很哦。


    三、命令列介面

    上面我們看到了,main和命令列介面根本就是同一個類,只是這個類做了兩種執行方式的相容。

使用python -m unittest -h可以查看協助命令,其中python -m unittest discover是命令列的另一分支,後面討論,它也有自己的協助命令,即也在後面加上-h

    具體的命令可自行研究。


    四、測試發現

    測試發現指,提供起始目錄,自動搜尋該目錄下的測試案例。與loadTestsFromModule等相同的是都由TestLoader提供,用來載入測試對象,返回一個TestSuit對象(包裹了搜尋到的測試對象)。不同的是,測試發現可以針對一個給定的目錄來搜尋。

    也可以通過上面提到的命令列來自動探索:python -m unittest discover **

    可以指定下面的參數:-s 起始目錄(.)  -t 頂級目錄(.)  -p 測試檔案的模式比對

    過程簡要描述如下:目錄:頂級目錄/起始目錄,該目錄應該是一個可匯入的包,即該目錄下應該提供__init__.py檔案。在該目錄下。使用-p模式比對test用例所在的檔案,然後在從這些檔案中預設通過‘test’首碼來搜集test方法構建test執行個體,最終返回一個test執行個體集合的suit對象。

    

    五、一些好用的修飾器

    unittest支援跳過某些測試方法甚至整個測試類別,也可以標誌某些方法是期待的不通過,這樣如果不通過的話就不會列入failure的計數中。等等這些都是通過裝飾器來實現的。讓我們把本文開篇的基礎的例子重用一下,將test.py改成下面這樣:

#test.py檔案提供了通過unittest構建的測試代碼    #coding:utf-8from exam import operatorimport unittest,sysclass TestOperator(unittest.TestCase):        def setUp(self):               #test fixture        self.oper = operator(10,0)        @unittest.skip("I TRUST IT")           #    def test_add(self):            #test case        self.assertEqual(self.oper.add(), 10, u"加法基礎功能不滿足要求")            @unittest.skipIf(sys.platform == ‘win32‘, "it just only run in Linux!")    def test_divide(self):        self.assertRaises(ZeroDivisionError, self.oper.divide())        #def tearDown(self):        #pass        if __name__ == "__main__":    unittest.main(verbosity=2)

 再次運行之後,結果如下:

test_add (__main__.TestOperator) ... skipped ‘I TRUST IT‘test_divide (__main__.TestOperator) ... skipped ‘it just only run in Linux!‘----------------------------------------------------------------------Ran 2 tests in 0.000sOK (skipped=2)

    unittest.skipUnless(condition, reason):如果condition為真則不會跳過該測試

    unittest.expectedFailure():將該test標誌為期待的失敗。之後如果該測試不符合預期或引發異常,則不會計入失敗數

    

    一直很崇拜裝飾器,不如就在此領略一下大神的風采,讓我們看看到底裝飾器是否必要,主要應用情境是什麼。就先拿裡面最簡單的skip來看吧:

def skip(reason):    """    Unconditionally skip a test.    """    def decorator(test_item):        if not isinstance(test_item, (type, types.ClassType)):            @functools.wraps(test_item)            def skip_wrapper(*args, **kwargs):                raise SkipTest(reason)            test_item = skip_wrapper        test_item.__unittest_skip__ = True        test_item.__unittest_skip_why__ = reason        return test_item    return decorator

    可以看出,如果該skip裝飾器修飾測試類別時,直接添加__unittest_skip__屬性即可,這會在執行個體運行中判斷。如果修飾測試方法時,會將修飾的方法替代為一個觸發SkipTest異常的方法,並同樣給修飾的方法添加__unittest_skip__屬性。

    添加的屬性在測試執行個體運行時會用到,在TestCase類提供的run方法中作判斷:

 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)            finally:                result.stopTest(self)            return

    如果測試方法或其所屬的類存在__unittest_skip__屬性為真,則會跳過該測試。通過上面我們看出,執行個體運行時只會檢查__unittest_skip__屬性值而並不會抓取SkipTest異常,那為什麼skip裝飾器中要對修飾的函數進行替換的操作呢?

    想不通,注釋掉if塊,程式依然可以啟動並執行好好的,留個疑點吧!

    

本文出自 “無名” 部落格,請務必保留此出處http://xdzw608.blog.51cto.com/4812210/1612063

python unittest架構

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.