標籤: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架構