unittest作為一個(gè)python中的基本模塊,是其他框架和工具的基礎(chǔ),官方文檔神馬的最實(shí)用了:https://docs.python.org/2/library/unittest.html
對官方文檔做了一個(gè)粗略的翻譯,稍有調(diào)整。
python版本:2.7
基本概念
- test fixture
一個(gè)test fixture代表執(zhí)行一個(gè)或者多個(gè)測試時(shí)需要準(zhǔn)備環(huán)境,以及相關(guān)聯(lián)的清理環(huán)境的工作。這包含很多內(nèi)容,比如創(chuàng)建臨時(shí)的數(shù)據(jù)庫、目錄等。 - test case
一個(gè)test case就是測試用例,測試當(dāng)中的最小單元。unittest提供一個(gè)基本的類TestCase,用來創(chuàng)建一個(gè)test case。 - test suite
test suite是一組test case或者test suite的集合,也可以兩者都有。用來將需要一同執(zhí)行的測試用例聚合到一起。 - test runner
一個(gè)test runner是用來執(zhí)行測試用例的,對測試進(jìn)行編排并把結(jié)果返回給用戶。
一個(gè)TestCase的實(shí)例應(yīng)該是完全獨(dú)立的,可以獨(dú)立的運(yùn)行測試,也可以和其他測試用例進(jìn)行組合。最簡單的TestCase子類簡單的重寫runTest()方法執(zhí)行特定代碼就可以:
import unittest
class JustForTest(unittest.TestCase):
def runTest(self):
length = 10
self.assertEqual(10, length)
通過unittest模塊可以在命令行下對一個(gè)模塊、一個(gè)類或者是一個(gè)方法執(zhí)行測試,比如對這個(gè)test.py模塊執(zhí)行單元測試:
? python -m unittest test
.
---------------------------------------------------------
Ran 1 test in 0.000s
OK
#
一個(gè)簡單用例
unittest模塊為構(gòu)建和執(zhí)行測試提供了非常豐富的工具集,下面這個(gè)例子用來測試三個(gè)字符串的方法:
import unittest
class TestStringMethods(unittest.TestCase):
def test_upper(self):
self.assertEqual('foo'.upper(), 'FOO')
def test_isupper(self):
self.assertTrue('FOO'.isupper())
self.assertFalse('Foo'.isupper())
def test_split(self):
s = 'hello world'
self.assertEqual(s.split(), ['hello', 'world'])
with self.assertRaises(TypeError):
s.split(2)
if __name__ == '__main__':
unittest.main()
測試用例是unittest.TestCase的子類,三個(gè)獨(dú)立的測試是以test開頭的。用這樣的命名規(guī)則來約定哪些方法是test runner需要執(zhí)行的。
每個(gè)測試的關(guān)鍵在于調(diào)用assertEqual()進(jìn)行檢查期望的結(jié)果;assertTrue()和assertFalse()用來判斷一個(gè)條件;assertRaises()用來確認(rèn)拋出了一個(gè)指定的異常。這些方法用來代替assert語句,test runner可以收集所有的結(jié)果并生成最后的報(bào)告。
代碼總13~14行是一個(gè)簡單的用來執(zhí)行測試的方式。unittest.main()給測試腳本提供了一個(gè)命令行接口。當(dāng)從命令行執(zhí)行的時(shí)候,上面的腳本會有如下輸出:
? python basic_example.py
...
----------------------------------------------------------------------
Ran 3 tests in 0.000s
OK
不使用unittest.main()的話也可以用其他方式替代,比如13、14行可以替換為:
#if __name__ == '__main__':
# unittest.main()
suite = unittest.TestLoader().loadTestsFromTestCase(TestStringMethods)
unittest.TextTestRunner(verbosity=2).run(suite)
可以得到更加清晰的輸出:
? python_unit_test_study python basic_example.py
test_isupper (__main__.TestStringMethods) ... ok
test_split (__main__.TestStringMethods) ... ok
test_upper (__main__.TestStringMethods) ... ok
----------------------------------------------------------------------
Ran 3 tests in 0.000s
OK
命令行接口
unittest模塊可以從命令行執(zhí)行模塊中的測試,可以是一個(gè)類也可以使單獨(dú)的測試用例:
? python -m unittest basic_example.TestStringMethod$
...
----------------------------------------------------------------------
Ran 3 tests in 0.000s
OK
? python_unit_test_study python -m unittest basic_example.TestStringMethods
.test_isupper
.
----------------------------------------------------------------------
Ran 1 test in 0.000s
OK
也可以傳遞一個(gè)模塊列表,執(zhí)行多個(gè)模塊中的測試用例:
? python -m unittest test_module1 test_module2
-v參數(shù)可以獲得更多的內(nèi)容:
? python -m unittest -v basic_example
test_isupper (basic_example.TestStringMethods) ... ok
test_split (basic_example.TestStringMethods) ... ok
test_upper (basic_example.TestStringMethods) ... ok
----------------------------------------------------------------------
Ran 3 tests in 0.000s
OK
-h可以獲得完整命令行參數(shù)的幫助說明:
? python -m unittest -h
Usage: python -m unittest [options] [tests]
Options:
-h, --help Show this message
-v, --verbose Verbose output
-q, --quiet Minimal output
-f, --failfast Stop on first failure
-c, --catch Catch control-C and display results
-b, --buffer Buffer stdout and stderr during test runs
Examples:
python -m unittest test_module - run tests from test_module
python -m unittest module.TestClass - run tests from module.TestClass
python -m unittest module.Class.test_method - run specified test method
[tests] can be a list of any number of test modules, classes and test
methods.
Alternative Usage: python -m unittest discover [options]
Options:
-v, --verbose Verbose output
-f, --failfast Stop on first failure
-c, --catch Catch control-C and display results
-b, --buffer Buffer stdout and stderr during test runs
-s directory Directory to start discovery ('.' default)
-p pattern Pattern to match test files ('test*.py' default)
-t directory Top level directory of project (default to
start directory)
For test discovery all test modules must be importable from the top
level directory of the project.
測試發(fā)現(xiàn)
unittest支持非常簡單的測試發(fā)現(xiàn)。為了兼容測試發(fā)現(xiàn),所有的測試文件(test_xxx.py)必須是可從項(xiàng)目的頂級目錄導(dǎo)入的模塊或包。測試發(fā)現(xiàn)由TestLoader.discover()實(shí)現(xiàn),但是可以通過命令行使用,基本的用法如下:
? python -m unittest discover
.....
----------------------------------------------------------------------
Ran 5 tests in 0.000s
OK
? python_unit_test_study python -m unittest discover -h
Usage: python -m unittest discover [options]
Options:
-h, --help show this help message and exit
-v, --verbose Verbose output
-f, --failfast Stop on first fail or error
-c, --catch Catch Ctrl-C and display results so far
-b, --buffer Buffer stdout and stderr during tests
-s START, --start-directory=START
Directory to start discovery ('.' default)
-p PATTERN, --pattern=PATTERN
Pattern to match tests ('test*.py' default)
-t TOP, --top-level-directory=TOP
Top level directory of project (defaults to start
directory)
組織代碼
單元測試最基本的結(jié)構(gòu)應(yīng)該是測試用例——必須設(shè)置并檢查正確性的單獨(dú)場景。在unittest中,測試用例就是unittest的TestCase類的一個(gè)實(shí)例。寫測試代碼時(shí),必須書寫TestCase的子類,或者使用FunctionTestCase。
文章開頭講過,最基本的TestCase子類就是重寫runTest()方法即可,在runTest()中執(zhí)行響應(yīng)的測試代碼。比如:
import unittest
class DefaultWidgetSizeTestCase(unittest.TestCase):
def runTest(self):
widget = Widget('The widget')
self.assertEqual(widget.size(), (50, 50), 'incorrect default size')
為了執(zhí)行測試內(nèi)容,使用TestCase基類提供的assert*()中的方法來檢查結(jié)果。如果測試失敗,就會拋出異常,unittest會將這個(gè)測試標(biāo)記為Failure。而其他的異常都會被當(dāng)做Error處理。這可以幫助我們判斷代碼中的問題:failure是由于測試結(jié)果引起的錯(cuò)誤-期望值是5得到的卻是6。而Errors是由于代碼本身的錯(cuò)誤引起,比如常見的TypeError等。
當(dāng)我們對同一類型的測試內(nèi)容寫測試代碼時(shí),很多測試方法的構(gòu)建和初始化可能是重復(fù)的,這種情況下我們可以使用setUp()方法,setUp()可以將初始化統(tǒng)一抽離出來,在一個(gè)測試方法執(zhí)行前都會先執(zhí)行,比如:
如果setUp()方法拋出異常的話,測試框架認(rèn)為測試過程遇到了錯(cuò)誤,runTest()方法不會被執(zhí)行的。
import unittest
class SimpleWidgetTestCase(unittest.TestCase):
def setUp(self):
self.widget = Widget("The widget")
class DefaultWidgetSizeTestCase(SimpleWidgetTestCase):
def runTest(self):
self.assertEqual(self.widget.size(), (50, 50),
'incorrect default size')
class WidgetResizeTestCase(SimpleWidgetTestCase):
def runTest(self):
self.widget.resize(100, 150)
self.assertEqual(self.widget.size(), (100, 150),
'wrong size after resize')
執(zhí)行結(jié)果:
? python -m unittest test_widget
EE
======================================================================
ERROR: runTest (test_widget.DefaultWidgetSizeTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
File "test_widget.py", line 5, in setUp
self.widget = Widget("The widget")
NameError: global name 'Widget' is not defined
======================================================================
ERROR: runTest (test_widget.WidgetResizeTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
File "test_widget.py", line 5, in setUp
self.widget = Widget("The widget")
NameError: global name 'Widget' is not defined
----------------------------------------------------------------------
Ran 2 tests in 0.001s
FAILED (errors=2)
同樣的,我們可以使用tearDown()方法在runTest()執(zhí)行結(jié)束后進(jìn)行清理,如果setUp()方法執(zhí)行成功,不論runTest()是否成功,tearDown()都會執(zhí)行。
這樣一個(gè)初始化、清理的完整的測試環(huán)境叫做fixture,通常很多小的測試使用的是相同的fixture的,
import unittest
class WidgetTestCase(unittest.TestCase):
def setUp(self):
self.widget = Widget('The widget')
def tearDown(self):
self.widget.dispose()
self.widget = None
def test_default_size(self):
self.assertEqual(self.widget.size(), (50, 50),
'incorrect default size')
def test_resize(self):
self.widget.resize(100, 150)
self.assertEqual(self.widget.size(), (100, 150),
'wrong size after resize')
這里沒有使用runTest()方法,但是使用了其他兩個(gè)測試方法作為替代。測試類實(shí)例會執(zhí)行每一個(gè)test_*()方法,self.widget會對每一個(gè)實(shí)例進(jìn)行創(chuàng)建和銷毀。這種構(gòu)建方法,創(chuàng)建實(shí)例的時(shí)候我們需要具體的指出需要執(zhí)行的測試方法:
defaultSizeTestCase = WidgetTestCase('test_default_size')
resizeTestCase = WidgetTestCase('test_resize')
unittest提供test suite(測試套件)可以將測試用例的實(shí)例很好的按照功能特性進(jìn)行合理的組織,在unittest中通過TestSuite類實(shí)現(xiàn):
widgetTestSuite = unittest.TestSuite()
widgetTestSuite.addTest(WidgetTestCase('test_default_size'))
widgetTestSuite.addTest(WidgetTestCase('test_resize'))
為了方便測試,在每一個(gè)模塊中提供一個(gè)可調(diào)用的預(yù)構(gòu)建的test suite是一個(gè)不錯(cuò)的主意:
def suite():
suite = unittest.TestSuite()
suite.addTest(WidgetTestCase('test_default_size'))
suite.addTest(WidgetTestCase('test_resize'))
return suite
也可以這樣寫:
def suite():
tests = ['test_default_size', 'test_resize']
return unittest.TestSuite(map(WidgetTestCase, tests))
由于使用相似名字的測試函數(shù)來創(chuàng)建一個(gè)TestCase子類是非常通用的模式,unittest提供了一個(gè)TestLoader類可以自動(dòng)的創(chuàng)建測試套件并用獨(dú)立的測試進(jìn)行填充,比如:
suite = unittest.TestLoader().loadTestsFromTestCase(WidgetTestCase)
這就創(chuàng)建了一個(gè)測試套件,將會執(zhí)行WidgetTestCase.test_default_size()和WidgetTestCase.test_resize。TestLoader將會自動(dòng)的識別以test_開頭的測試方法。
各個(gè)測試的執(zhí)行順序是由測試的函數(shù)名按照字符串內(nèi)建順序執(zhí)行的。
測試套件本身也可以像測試用例一樣組織起來:
suite1 = module1.TheTestSuite()
suite2 = module2.TheTestSuite()
alltests = unittest.TestSuite([suite1, suite2])
我們可以將測試用例與測試套件的定義放在與測試代碼相同的模塊中,但將測試代碼放在單獨(dú)的模塊中又幾個(gè)好處:
- 測試模塊可以獨(dú)立的通過命令行進(jìn)行執(zhí)行
- 測試代碼可以更容易的與發(fā)布代碼分離
- 沒有必要的情況下不必為了適應(yīng)被測試代碼而頻繁更改測試代碼
- 測試的代碼更容易重構(gòu)
。。。等等
跳過測試以及異常測試
unittest支持跳過單獨(dú)的測試方法甚至是整個(gè)測試類。也支持將一個(gè)測試標(biāo)記為“expected failure”。
跳過一個(gè)測試可以使用skip()描述符或者它的一個(gè)條件語句,最基本的skip使用像這樣:
import unittest
import sys
class MyTestCase(unittest.TestCase):
@unittest.skip('demonstrating sipping')
def test_nothing(self):
self.fail("shouldn't happen")
@unittest.skipUnless(sys.platform.startswith("win"), 'requires Windows')
def test_windows_support(self):
pass
執(zhí)行結(jié)果:
? python_unit_test_study python -m unittest -v mylib
test_nothing (mylib.MyTestCase) ... skipped 'demonstrating sipping'
test_windows_support (mylib.MyTestCase) ... skipped 'requires Windows'
----------------------------------------------------------------------
Ran 2 tests in 0.000s
OK (skipped=2)
如果想跳過整個(gè)測試類,方法是和上面是一樣的。
期望失敗通過expectedFailure()描述符實(shí)現(xiàn)的:
@unittest.expectedFailure
def test_format(self):
pass
執(zhí)行結(jié)果:
? python -m unittest -v mylib
test_format (mylib.MyTestCase) ... unexpected success
----------------------------------------------------------------------
Ran 1 test in 0.000s
OK (unexpected successes=1)
被跳過的測試不會執(zhí)行setUp和tearDown,被跳過的類也不會執(zhí)行setUpClass和tearDownClass。