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-8

class 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-8

from exam import operator
import unittest

class 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) ... ok
test_divide (__main__.TestOperator) ... ok
----------------------------------------------------------------------
Ran 2 tests in 0.000s
OK
  • 測試類需要繼承自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-8

from exam import operator
import unittest,sys

class 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.000s

OK (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塊,程序依然可以運行的好好的,留個疑點吧!

    

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章