一、什麼是單元測試
單元測試是用來對一個模組、一個函數或者一個類來進行正確性檢驗的測試工作。
比如對於函數abs(),我們可以編寫的測試案例為:
(1)輸入正數,比如1、1.2、0.99,期待傳回值與輸入相同
(2)輸入複數,比如-1、-1.2、-0.99,期待傳回值與輸入相反
(3)輸入0,期待返回0
(4)輸入非數實值型別,比如None、[]、{}、期待拋出TypeError
把上面這些測試案例放到一個測試模組裡,就是一個完整的單元測試
二、unittest工作原理
unittest中最核心的四部分是:TestCase,TestSuite,TestRunner,TestFixture
(1)一個TestCase的執行個體就是一個測試案例。測試案例就是指一個完整的測試流程,包括測試前準備環境的搭建(setUp),執行測試代碼(run),以及測試後環境的還原(tearDown)。元測試(unit test)的本質也就在這裡,一個測試案例是一個完整的測試單元,通過運行這個測試單元,可以對某一個問題進行驗證。
(2)而多個測試案例集合在一起,就是TestSuite,而且TestSuite也可以嵌套TestSuite。
(3)TestLoader是用來載入TestCase到TestSuite中的。
(4)TextTestRunner是來執行測試案例的,其中的run(test)會執行TestSuite/TestCase中的run(result)方法
(5)測試的結果會儲存到TextTestResult執行個體中,包括運行了多少測試案例,成功了多少,失敗了多少等資訊。
綜上,整個流程就是首先要寫好TestCase,然後由TestLoader載入TestCase到TestSuite,然後由TextTestRunner來運行TestSuite,啟動並執行結果儲存在TextTestResult中,整個過程整合在unittest.main模組中。
三、下面舉兩個執行個體,來看看unittest如何測試一個簡單的函數
(1)編寫一個Dict類,這個類的行為和dict一致,但是可以通過屬性來訪問例如
>>> d = Dict(a=1, b=2)>>> d['a']1>>> d.a1
mydict.py代碼如下:
class Dict(dict): def __init__(self, **kw): super(Dict, self).__init__(**kw) def __getattr__(self, key): try: return self[key] except KeyError: raise AttributeError(r"'Dict' object has no attribute '%s'" % key) def __setattr__(self, key, value): self[key] = value
用於測試的檔案mydict_test.py代碼如下:
import unittestfrom mydict import Dict class TestDict(unittest.TestCase): def test_init(self): d = Dict(a=1, b='test') self.assertEqual(d.a, 1) # 判斷d.a是否等於1 self.assertEqual(d.b, 'test') # 判斷d.b是否等於test self.assertTrue(isinstance(d, dict)) # 判斷d是否是dict類型 def test_key(self): d = Dict() d['key'] = 'value' self.assertEqual(d.key, 'value') def test_attr(self): d = Dict() d.key = 'value' self.assertTrue('key' in d) self.assertEqual(d['key'], 'value') def test_keyerror(self): d = Dict() with self.assertRaises(KeyError): # 通過d['empty']訪問不存在的key時,斷言會拋出keyerror value = d['empty'] def test_attrerror(self): d = Dict() with self.assertRaises(AttributeError): # 通過d.empty訪問不存在的key時,我們期待拋出AttributeError value = d.empty if __name__ == '__main__': unittest.main()
直接把mydict_test.py當普通的Python指令碼運行即可
輸出:
.....----------------------------------------------------------------------Ran 5 tests in 0.000s OK
(2)測一個簡單的加減乘除介面
mathfunc.py檔案代碼如下:
def add(a, b): return a + b def minus(a, b): return a - b def multi(a, b): return a * b def divide(a, b): return a / b
test_mathfunc.py檔案代碼如下:
import unittestfrom mathfunc import * class TestMathFunc(unittest.TestCase): def test_add(self): self.assertEqual(3, add(1, 2)) self.assertNotEqual(3, add(2, 2)) def test_minus(self): self.assertEqual(1, minus(3, 2)) def test_multi(self): self.assertEqual(6, multi(3, 2)) def test_divide(self): self.assertEqual(2, divide(6, 3)) self.assertEqual(2.5, divide(5, 2)) if __name__ == '__main__':unittest.main()
輸出:
.F..======================================================================FAIL: test_divide (__main__.TestDict)----------------------------------------------------------------------Traceback (most recent call last): File "D:/pythonWorkspace/test_mathfunc.py", line 20, in test_divide self.assertEqual(2.5, divide(5, 2))AssertionError: 2.5 != 2 ----------------------------------------------------------------------Ran 4 tests in 0.000s FAILED (failures=1)
可以看到一共運行了4個測試,失敗了1個,並且給出了失敗原因,2.5!=2,也就是說我們的divide方法是有問題的。
關於輸出的幾點說明:
1、在第一行給出了每一個用例執行的結果的標識,成功是.,失敗是F,出錯是E,跳過是S。從上面可以看出,測試的執行跟方法的順序沒有關係,divide方法寫在了第4個,但是卻在第2個執行。
2、每個測試方法均以test開頭,否則不能被unittest識別
3、在uniitest.main()中加verbosity參數可以控制輸出的錯誤報表的詳細程度,預設是1,如果設為0, 則不輸出每一用例的執行結果,即沒有上面的結果中的第1行,如果設為2,則輸出詳細的執行結果,如下所示:
test_add (__main__.TestMathFunc) ... oktest_divide (__main__.TestMathFunc) ... FAILtest_minus (__main__.TestMathFunc) ... oktest_multi (__main__.TestMathFunc) ... ok ======================================================================FAIL: test_divide (__main__.TestMathFunc)----------------------------------------------------------------------Traceback (most recent call last): File "D:/pythonWorkspace/test_mathfunc.py", line 20, in test_divide self.assertEqual(2.5, divide(5, 2))AssertionError: 2.5 != 2 ----------------------------------------------------------------------Ran 4 tests in 0.000s FAILED (failures=1)
四、組織TestSuite
上面的測試案例在執行的時候沒有按照順序執行,如果想要讓用例按照你設定的順序執行就用到了TestSuite。我們添加到TestSuite中的case是會按照添加的順序執行的。
現在我們只有一個測試檔案,如果有多個測試檔案,也可以用TestSuite組織起來。
繼續上面第二加減乘除的例子,現在再建立一個檔案,test_suite.py,代碼如下:
# coding=utf-8import unittestfrom test_mathfunc import TestMathFunc if __name__ == '__main__': suite = unittest.TestSuite() tests = [TestMathFunc("test_add"), TestMathFunc("test_minus"), TestMathFunc("test_divide")] suite.addTests(tests) runner = unittest.TextTestRunner(verbosity=2) runner.run(suite)
執行結果如下:
test_add (test_mathfunc.TestMathFunc) ... oktest_minus (test_mathfunc.TestMathFunc) ... oktest_divide (test_mathfunc.TestMathFunc) ... FAIL ======================================================================FAIL: test_divide (test_mathfunc.TestMathFunc)----------------------------------------------------------------------Traceback (most recent call last): File "D:\pythonWorkspace\HTMLTest\test_mathfunc.py", line 20, in test_divide self.assertEqual(2.5, divide(5, 2))AssertionError: 2.5 != 2 ----------------------------------------------------------------------Ran 3 tests in 0.000s FAILED (failures=1)
五、將結果輸出到檔案
現在我們的測試結果只能輸出到控制台,現在我們想將結果輸出到檔案中以便後續可以查看。
將test_suite.py進行一點修改,代碼如下:
# coding=utf-8 import unittestfrom test_mathfunc import TestMathFunc if __name__ == '__main__': suite = unittest.TestSuite() tests = [TestMathFunc("test_add"), TestMathFunc("test_minus"), TestMathFunc("test_divide")] suite.addTests(tests) with open('UnittestTextReport.txt', 'a') as f: runner = unittest.TextTestRunner(stream=f, verbosity=2) runner.run(suite)
運行該檔案,就會發現目錄下產生了'UnittestTextReport.txt,所有的執行報告均輸出到了此檔案中。
六、test fixture的setUp和tearDown
當遇到要啟動一個資料庫這種情況時,只想在開始時串連上資料庫,在結束時關閉串連。那麼可以使用setUp和tearDown函數。
class TestDict(unittest.TestCase): def setUp(self): print 'setUp...' def tearDown(self): print 'tearDown...'
這兩個方法在每個測試方法執行前以及執行後執行一次,setUp用來為測試準備環境,tearDown用來清理環境,以備後續的測試。
如果想要在所有case執行之前準備一次環境,並在所有case執行結束之後再清理環境,我們可以用setUpClass()與tearDownClass(),代碼格式如下:
class TestMathFunc(unittest.TestCase): @classmethod def setUpClass(cls): print "setUp" @classmethod def tearDownClass(cls): print "tearDown"
七、跳過某個case
unittest提供了幾種方法可以跳過case
(1)skip裝飾器
代碼如下
# coding=utf-8import unittestfrom mathfunc import * class TestMathFunc(unittest.TestCase): ..... @unittest.skip("i don't want to run this case.") def test_minus(self): self.assertEqual(1, minus(3, 2))
輸出:
test_add (test_mathfunc.TestMathFunc) ... oktest_minus (test_mathfunc.TestMathFunc) ... skipped "i don't want to run this case."test_divide (test_mathfunc.TestMathFunc) ... FAIL ======================================================================FAIL: test_divide (test_mathfunc.TestMathFunc)----------------------------------------------------------------------Traceback (most recent call last): File "D:\pythonWorkspace\HTMLTest\test_mathfunc.py", line 28, in test_divide self.assertEqual(2.5, divide(5, 2))AssertionError: 2.5 != 2 ----------------------------------------------------------------------Ran 3 tests in 0.000s FAILED (failures=1, skipped=1)
skip裝飾器一共有三個
unittest,skip(reason):無條件跳過
unittest.skipIf(condition, reason):當condition為True時跳過
unittest.skipUnless(condition, reason):當condition為False時跳過
(2)TestCase.skipTest()方法
class TestMathFunc(unittest.TestCase):...def test_minus(self): self.skipTest('do not run this.') self.assertEqual(1, minus(3, 2))
輸出:
test_add (test_mathfunc.TestMathFunc) ... oktest_minus (test_mathfunc.TestMathFunc) ... skipped 'do not run this.'test_divide (test_mathfunc.TestMathFunc) ... FAIL ======================================================================FAIL: test_divide (test_mathfunc.TestMathFunc)----------------------------------------------------------------------Traceback (most recent call last): File "D:\pythonWorkspace\HTMLTest\test_mathfunc.py", line 20, in test_divide self.assertEqual(2.5, divide(5, 2))AssertionError: 2.5 != 2 ----------------------------------------------------------------------Ran 3 tests in 0.000s FAILED (failures=1, skipped=1)
八、用HTMLTestRunner輸出漂亮的HTML報告
txt格式的文本執行報告過於簡陋,這裡我們學習一下藉助HTMLTestRunner產生HTML報告。首先需要下載HTMLTestRunner.py,並放到目前的目錄下,或者python目錄下的Lib中,就可以匯入運行了。
下載地址:http://tungwaiyip.info/software/HTMLTestRunner.html
將test_suite.py代碼修改如下:
# coding=utf-8 import unittestfrom test_mathfunc import TestMathFuncfrom HTMLTestRunner import HTMLTestRunner if __name__ == '__main__': suite = unittest.TestSuite() tests = [TestMathFunc("test_add"), TestMathFunc("test_minus"), TestMathFunc("test_divide")] suite.addTests(tests) with open('HTMLReport.html', 'w') as f: runner = HTMLTestRunner(stream=f, title = 'MathFunc Test Report', description='generated by HTMLTestRunner.', verbosity=2 ) runner.run(suite)
執行後,控制台輸出如下:
ok test_add (test_mathfunc.TestMathFunc)F test_divide (test_mathfunc.TestMathFunc) Time Elapsed: 0:00:00.001000
產生的html:
九、總結
1、unittest是python內建的單元測試架構,我們可以用其來作為我們自動化測試架構的用例組織執行架構。
2、unittest的流程:寫好TestCase,然後由TestLoader載入TestCase到TestSuite,然後由TextTestRunner來運行TestSuite,啟動並執行結果儲存在TextTestResult中,我們通過命令列或者unittest.main()執行時,main會調用TextTestRunner中的run來執行,或者我們可以直接通過TextTestRunner來執行用例。
3、一個class繼承unittest.TestCase即是一個TestCase,其中以 test 開頭的方法在load時被載入為一個真正的TestCase。
4、verbosity參數可以控制執行結果的輸出,0 是簡單報告、1 是一般報告、2 是詳細報告。
5、可以通過addTest和addTests向suite中添加case或suite,可以用TestLoader的loadTestsFrom__()方法。
6、用 setUp()、tearDown()、setUpClass()以及 tearDownClass()可以在用例執行前布置環境,以及在用例執行後清理環境
7、我們可以通過skip,skipIf,skipUnless裝飾器跳過某個case,或者用TestCase.skipTest方法。
8、參數中加stream,可以將報告輸出到檔案:可以用TextTestRunner輸出txt報告,以及可以用HTMLTestRunner輸出html報告。