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