32.Python的單元測試工具——unittest(初級)
簡介
unittest是Python的內建模組,是Python單元測試的事實標準,也叫PyUnit。使用unittest之前,先了解如下幾個概念:
- test case:測試用例,可以通過建立
unitest.TestCase
類的子類建立一個測試用例。 - test fixture:包含執行測試用例前的測試準備工作、測試用例執行後的清理工作(分別對應
TestCase
中的setUp()
和tearDown()
方法),測試準備和測試清理的目的是保證每個測試用例執行前後的系統狀態一致。 - test suite:測試套,是測試用例、測試套或者兩者的集合,用來將有關聯的測試項打包。
- test runner:負責執行測試並將結果展示給使用者,可以展示圖形或文字形式(
unittest.TextTestRunner
run()
,接受一個unittest.TestSuite
或unittest.TestCase
例項作為引數,執行對應測試專案後返回測試結果unittest.TestResult
物件。
基本使用方法
定義測試用例的方法如下:
#unit.py
import unittest
class TestStringMethods(unittest.TestCase):
def test_upper(self):
self.assertEqual('Loo'.upper(), 'LOO' )
def test_isupper(self):
self.assertTrue('LOO'.isupper())
self.assertFalse('Loo'.isupper())
def test_split(self):
s = 'Mars Loo'
with self.assertRaises(TypeError):
s.split(2)
if __name__ == '__main__':
unittest.main()
執行指令碼:
$ python unit.py
...
----------------------------------------------------------------------
Ran 3 tests in 0.000s
OK
每一個測試專案的函式定義以test開頭命名,這樣test runner就知道哪些函式是應該被執行的。上面的例子展示了驗證測試結果常用的三種方法:
assertEqual(a, b)
:比較a
==b
。assertTrue(exp)
和assertFalse(exp)
:驗證bool
(exp)為True
或者False
。assertRaises(Exception)
:驗證Exception
被丟擲。
之所以不使用Python內建的assert丟擲異常,是因為test runner需要根據這些封裝後的方法丟擲的異常做測試結果統計。
unittest.main()
方法會在當前模組尋找所有unittest.TestCase
的子類,並執行它們中的所有測試專案。使用-v引數可以看到更詳細的測試執行過程:
$ python unit.py -v
test_isupper (__main__.TestStringMethods) ... ok
test_split (__main__.TestStringMethods) ... ok
test_upper (__main__.TestStringMethods) ... ok
----------------------------------------------------------------------
Ran 3 tests in 0.000s
OK
也可以修改最後兩行成如下程式碼:
suite = unittest.TestLoader().loadTestsFromTestCase(TestStringMethods)
unittest.TextTestRunner(verbosity=2).run(suite)
測試結果如下:
$ python unit.py
test_isupper (__main__.TestStringMethods) ... ok
test_split (__main__.TestStringMethods) ... ok
test_upper (__main__.TestStringMethods) ... ok
----------------------------------------------------------------------
Ran 3 tests in 0.000s
OK
從命令列執行unittest
$ python -m unittest unit #直接執行模組unit中的測試用例
$ python -m unittest unit.TestStringMethods #執行模組中的某個類
$ python -m unittest unit.TestStringMethods.test_upper #執行某個單獨的測試方法
混合執行測試模組、類以及測試方法也是可以的。
如果要檢視unittest模組命令列的更多引數資訊,使用-h
引數:
$ python -m unittest -h
-b
引數:只在測試用例fail或者error時顯示它的stdout和stderr,否則不會顯示。-f
引數:如果有一個測試用例fail或者出現error,立即停止測試。-c
引數:捕捉Control-C訊號,並顯示測試結果。
自動發現測試用例
unittest能夠自動發現測試用例。為了讓測試用例能夠被自動發現,測試檔案需要是在專案目錄中可以import的module或者package,比如如下目錄結構:
unittest
├── test_a
│ ├── __init__.py
│ └── test_a.py
└── test_b.py
在unittest目錄中執行如下命令,即可執行test_a
這個package和test_b
這個module中的測試專案:
$ python -m unittest discover
..
----------------------------------------------------------------------
Ran 2 tests in 0.000s
OK
unittest discovery預設會搜尋名字命名符合test*的module或package,可以新增更多的引數:
-v
:詳細輸出。-s
:開始自動搜尋的目錄,預設是.
;這個引數也可以指向一個package名,而不是目錄,例如unittest.test_a。-p
:檔案匹配的模式,預設是test*.py
。-t
:專案頂級目錄,預設與開始自動搜尋的目錄相同。
比如:
$ python -m unittest discover -s test_a
.
----------------------------------------------------------------------
Ran 1 test in 0.000s
OK
測試用例發現通過import模組或package執行測試,例如foo/bar/baz.py
會被import為foo.bar.baz
。
測試程式碼的組織
測試用例一定要是自包含的,即測試用例既可以獨立執行,也可以和其他測試用例混合執行,測試用例執行前後不能影響系統狀態。
建議將被測試程式碼和測試程式碼分離,比如一個模組module.py
對應的單元測試的檔案是test_module.py
,這樣方便維護。
最簡單的測試用例定義,是一個unittest.TestCase
的子類只包含一個測試步驟,這個時候只需要定義一個runTest
方法,比如:
# unit.py
import unittest
class MyTestCase(unittest.TestCase):
def runTest(self):
self.assertEqual(1, 2, 'not equal')
執行測試後結果如下:
$ python -m unittest -v unit
runTest (unit.MyTestCase) ... FAIL
======================================================================
FAIL: runTest (unit.MyTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
File "unit.py", line 5, in runTest
self.assertEqual(1, 2, 'not equal')
AssertionError: not equal
----------------------------------------------------------------------
Ran 1 test in 0.001s
FAILED (failures=1)
如果assert*方法檢查失敗,會丟擲一個異常,unittest會將其算作失敗(failure)。任何其他異常都被unittest算作錯誤(error),比如:
#unit.py
import unittest
class MyTestCase(unittest.TestCase):
def runTest(self):
self.assertEqual(notexist, 2, 'not exist')
執行測試結果如下:
$ python -m unittest -v unit
runTest (unit.MyTestCase) ... ERROR
======================================================================
ERROR: runTest (unit.MyTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
File "unit.py", line 5, in runTest
self.assertEqual(notexist, 2, 'not exist')
NameError: global name 'notexist' is not defined
----------------------------------------------------------------------
Ran 1 test in 0.000s
FAILED (errors=1)
即failure通常是實際結果與預期結果不符,error通常是因為測試程式碼有bug導致。
如果很多測試專案的初始化準備工作類似,可以為他們定義同一個setUp方法,比如:
import unittest
class BaseTestCase(unittest.TestCase):
def setUp(self):
self._value = 12
class TestCase1(BaseTestCase):
def runTest(self):
self.assertEqual(self._value, 12, 'default value error')
class TestCase2(BaseTestCase):
def runTest(self):
self._value = 13
self.assertEqual(self._value, 13, 'change value fail')
如果基類BaseTestCase
的setUp
方法中丟擲異常,unittest不會繼續執行子類中的runTest
方法。
如果想在測試專案執行結果後進行現場清理,可以定義tearDown()
方法:
import unittest
class B(unittest.TestCase):
def setUp(self):
self._value = 1
def test_b(self):
self.assertEqual(self._value, 1)
def tearDown(self):
del self._value
setUp()
和tearDown()
方法的執行過程是:針對每一個測試專案,先執行setUp()
方法,如果成功,那麼繼續執行測試函式,最後不管測試函式是否執行成功,都執行tearDown()
方法;如果setUp()
方法失敗,則認為這個測試專案失敗,不會執行測試函式也不執行tearDown()
方法。
工作中很多測試專案依賴相同的測試夾具(setUp
和tearDown
),unittest支援像這樣定義測試用例:
import unittest
class TestCase1(unittest.TestCase):
def setUp(self):
self._value = 12
def test_default(self):
self.assertEqual(self._value, 12, 'default value error')
def test_change(self):
self._value = 13
self.assertEqual(self._value, 13, 'change value fail')
如果要執行指定的測試用例的話,可以使用TestSuite這個類,包含使用方法名作為引數宣告一個測試用例例項,比如:
import unittest
class TestCase1(unittest.TestCase):
def setUp(self):
self._value = 12
def test_default(self):
self.assertEqual(self._value, 12, 'default value error')
def test_change(self):
self._value = 13
self.assertEqual(self._value, 13, 'change value fail')
test_suite = unittest.TestSuite()
test_suite.addTest(TestCase1('test_default'))
test_runner = unittest.TextTestRunner()
test_runner.run(test_suite)
測試套也可以是測試套的集合,比如:
import unittest
class TestCase1(unittest.TestCase):
def setUp(self):
self._value = 12
def test_default(self):
self.assertEqual(self._value, 12, 'default value error')
def test_change(self):
self._value = 13
self.assertEqual(self._value, 13, 'change value fail')
test_suite1 = unittest.TestSuite()
test_suite1.addTest(TestCase1('test_default'))
test_suite2 = unittest.TestSuite()
test_suite2.addTest(TestCase1('test_change'))
test_suite = unittest.TestSuite([test_suite1, test_suite2])
test_runner = unittest.TextTestRunner()
test_runner.run(test_suite)
如果想執行測試類中的部分測試用例,可以採用如下方式:
def suite():
tests = ['test_default', 'test_change']
return unittest.TestSuite(map(TestCase1, tests))
test_runner = unittest.TextTestRunner()
test_runner.run(suite())
因為將一個測試用例類下面的所有測試步驟都執行一遍的情況非常普遍,unittest提
供了TestLoader
類,它的loadTestsFromTestCase()
方法會在一個TestCase
類中尋找所有以test開頭的函式定義,並將他們新增到測試套中,這些函式會按照其名字的字串排序順序執行,比如:
import unittest
class TestCase1(unittest.TestCase):
def setUp(self):
self._value = 12
def test_default(self):
self.assertEqual(self._value, 12, 'default value error')
def test_change(self):
self._value = 13
self.assertEqual(self._value, 13, 'change value fail')
test_suite = unittest.TestLoader().loadTestsFromTestCase(TestCase1)
unittest.TextTestRunner(verbosity=2).run(test_suite)
忽略測試用例及假設用例失敗
有些情況下需要忽略執行某些測試用例或者測試類,這個時候可以使用unittest.skip
裝飾器及其變種。需要特別注意的是,可以通過skip某個測試類的setUp()
方法而跳過整個測試類的執行,比如:
import unittest, sys
version = (0, 1)
class HowToSkip(unittest.TestCase):
@unittest.skip('demonstrating skipping')
def test_nothing(self):
self.fail('will never be ran')
@unittest.skipIf(version < (1, 3),
'not supported version')
def test_format(self):
print 'your version is >= (1, 3)'
@unittest.skipUnless(sys.platform.startswith('win'),
'requires windows')
def test_winndows_support(self):
print 'support windows'
@unittest.skip('class can also be skipped')
class Skipped(unittest.TestCase):
def test_skip(self):
pass
class SkippedBySetUp(unittest.TestCase):
@unittest.skip('Skipped by setUp method')
def setUp(self):
pass
def test_dummy1(self):
print 'dummy1'
def test_dummy2(self):
print 'dummy2'
測試結果如下:
$ python -m unittest -v unit
test_format (unit4.HowToSkip) ... skipped 'not supported version'
test_nothing (unit4.HowToSkip) ... skipped 'demonstrating skipping'
test_winndows_support (unit4.HowToSkip) ... skipped 'requires windows'
test_skip (unit4.Skipped) ... skipped 'class can also be skipped'
test_dummy1 (unit4.SkippedBySetUp) ... skipped 'Skipped by setUp method'
test_dummy2 (unit4.SkippedBySetUp) ... skipped 'Skipped by setUp method'
----------------------------------------------------------------------
Ran 6 tests in 0.001s
OK (skipped=6)
特別地,被忽略的測試用例將不會執行他們的setUp()、tearDown()方法,被忽略的測試類將不會執行他們的setUpClass()、tearDownClass()方法(關於setUpClass()和tearDownClass()的詳細介紹,在下一篇部落格中)。
有的時候,明知道某些測試用例會失敗,這時可以使用unittest.expectedFailure
裝飾器,被期望失敗的測試用例不會加到測試結果的failure統計中,而是加到expected failure統計中,比如:
import unittest
class ExpectedFailure(unittest.TestCase):
@unittest.expectedFailure
def test_fail(self):
self.assertEqual(1, 2, 'not equal')
測試結果如下:
$ python -m unittest -v unit
test_fail (unit.ExpectedFailure) ... expected failure
----------------------------------------------------------------------
Ran 1 test in 0.000s
OK (expected failures=1)
如果被expectedFailure的測試用例成功了,會被加到unexpected success的計數中。
綜上所述,unittest執行測試用例結束後,有6種結束狀態:ok、failure、error、expected failure、skipped、unexpected success。實際工作中傳送自動化測試報告時,需要注意分別這些狀態的含義。
目前介紹的這些內容可以應付一些簡單工作,如果想要進一步學習unittest模組的類及更多的API和結果檢查方法,可以繼續看我的下一篇blog。
如果覺得我的文章對您有幫助,歡迎關注我(CSDN:Mars Loo的部落格)或者為這篇文章點贊,謝謝!