Python單元測(cè)試-unittest

unittest作為一個(gè)python中的基本模塊,是其他框架和工具的基礎(chǔ),官方文檔神馬的最實(shí)用了:https://docs.python.org/2/library/unittest.html
對(duì)官方文檔做了一個(gè)粗略的翻譯,稍有調(diào)整。

python版本:2.7

基本概念

  • test fixture
    一個(gè)test fixture代表執(zhí)行一個(gè)或者多個(gè)測(cè)試時(shí)需要準(zhǔn)備環(huán)境,以及相關(guān)聯(lián)的清理環(huán)境的工作。這包含很多內(nèi)容,比如創(chuàng)建臨時(shí)的數(shù)據(jù)庫(kù)、目錄等。
  • test case
    一個(gè)test case就是測(cè)試用例,測(cè)試當(dāng)中的最小單元。unittest提供一個(gè)基本的類TestCase,用來(lái)創(chuàng)建一個(gè)test case。
  • test suite
    test suite是一組test case或者test suite的集合,也可以兩者都有。用來(lái)將需要一同執(zhí)行的測(cè)試用例聚合到一起。
  • test runner
    一個(gè)test runner是用來(lái)執(zhí)行測(cè)試用例的,對(duì)測(cè)試進(jìn)行編排并把結(jié)果返回給用戶。

一個(gè)TestCase的實(shí)例應(yīng)該是完全獨(dú)立的,可以獨(dú)立的運(yùn)行測(cè)試,也可以和其他測(cè)試用例進(jìn)行組合。最簡(jiǎn)單的TestCase子類簡(jiǎn)單的重寫runTest()方法執(zhí)行特定代碼就可以:

    import unittest
    class JustForTest(unittest.TestCase):
        def runTest(self):
            length = 10
            self.assertEqual(10, length)

通過(guò)unittest模塊可以在命令行下對(duì)一個(gè)模塊、一個(gè)類或者是一個(gè)方法執(zhí)行測(cè)試,比如對(duì)這個(gè)test.py模塊執(zhí)行單元測(cè)試:

? python -m unittest test
.
---------------------------------------------------------
Ran 1 test in 0.000s

OK
#

一個(gè)簡(jiǎn)單用例

unittest模塊為構(gòu)建和執(zhí)行測(cè)試提供了非常豐富的工具集,下面這個(gè)例子用來(lái)測(cè)試三個(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()

測(cè)試用例是unittest.TestCase的子類,三個(gè)獨(dú)立的測(cè)試是以test開(kāi)頭的。用這樣的命名規(guī)則來(lái)約定哪些方法是test runner需要執(zhí)行的。
每個(gè)測(cè)試的關(guān)鍵在于調(diào)用assertEqual()進(jìn)行檢查期望的結(jié)果;assertTrue()和assertFalse()用來(lái)判斷一個(gè)條件;assertRaises()用來(lái)確認(rèn)拋出了一個(gè)指定的異常。這些方法用來(lái)代替assert語(yǔ)句,test runner可以收集所有的結(jié)果并生成最后的報(bào)告。

代碼總13~14行是一個(gè)簡(jiǎn)單的用來(lái)執(zhí)行測(cè)試的方式。unittest.main()給測(cè)試腳本提供了一個(gè)命令行接口。當(dāng)從命令行執(zhí)行的時(shí)候,上面的腳本會(huì)有如下輸出:

? 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í)行模塊中的測(cè)試,可以是一個(gè)類也可以使單獨(dú)的測(cè)試用例:

?  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è)模塊中的測(cè)試用例:

?  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ù)的幫助說(shuō)明:

? 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.

測(cè)試發(fā)現(xiàn)

unittest支持非常簡(jiǎn)單的測(cè)試發(fā)現(xiàn)。為了兼容測(cè)試發(fā)現(xiàn),所有的測(cè)試文件(test_xxx.py)必須是可從項(xiàng)目的頂級(jí)目錄導(dǎo)入的模塊或包。測(cè)試發(fā)現(xiàn)由TestLoader.discover()實(shí)現(xiàn),但是可以通過(guò)命令行使用,基本的用法如下:

?  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)

組織代碼

單元測(cè)試最基本的結(jié)構(gòu)應(yīng)該是測(cè)試用例——必須設(shè)置并檢查正確性的單獨(dú)場(chǎng)景。在unittest中,測(cè)試用例就是unittest的TestCase類的一個(gè)實(shí)例。寫測(cè)試代碼時(shí),必須書寫TestCase的子類,或者使用FunctionTestCase。
文章開(kāi)頭講過(guò),最基本的TestCase子類就是重寫runTest()方法即可,在runTest()中執(zhí)行響應(yīng)的測(cè)試代碼。比如:

    import unittest
    class DefaultWidgetSizeTestCase(unittest.TestCase):
        def runTest(self):
            widget = Widget('The widget')
            self.assertEqual(widget.size(), (50, 50), 'incorrect default size')

為了執(zhí)行測(cè)試內(nèi)容,使用TestCase基類提供的assert*()中的方法來(lái)檢查結(jié)果。如果測(cè)試失敗,就會(huì)拋出異常,unittest會(huì)將這個(gè)測(cè)試標(biāo)記為Failure。而其他的異常都會(huì)被當(dāng)做Error處理。這可以幫助我們判斷代碼中的問(wèn)題:failure是由于測(cè)試結(jié)果引起的錯(cuò)誤-期望值是5得到的卻是6。而Errors是由于代碼本身的錯(cuò)誤引起,比如常見(jiàn)的TypeError等。
當(dāng)我們對(duì)同一類型的測(cè)試內(nèi)容寫測(cè)試代碼時(shí),很多測(cè)試方法的構(gòu)建和初始化可能是重復(fù)的,這種情況下我們可以使用setUp()方法,setUp()可以將初始化統(tǒng)一抽離出來(lái),在一個(gè)測(cè)試方法執(zhí)行前都會(huì)先執(zhí)行,比如:
如果setUp()方法拋出異常的話,測(cè)試框架認(rèn)為測(cè)試過(guò)程遇到了錯(cuò)誤,runTest()方法不會(huì)被執(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()都會(huì)執(zhí)行。
這樣一個(gè)初始化、清理的完整的測(cè)試環(huán)境叫做fixture,通常很多小的測(cè)試使用的是相同的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')

這里沒(méi)有使用runTest()方法,但是使用了其他兩個(gè)測(cè)試方法作為替代。測(cè)試類實(shí)例會(huì)執(zhí)行每一個(gè)test_*()方法,self.widget會(huì)對(duì)每一個(gè)實(shí)例進(jìn)行創(chuàng)建和銷毀。這種構(gòu)建方法,創(chuàng)建實(shí)例的時(shí)候我們需要具體的指出需要執(zhí)行的測(cè)試方法:

    defaultSizeTestCase = WidgetTestCase('test_default_size')
    resizeTestCase = WidgetTestCase('test_resize')

unittest提供test suite(測(cè)試套件)可以將測(cè)試用例的實(shí)例很好的按照功能特性進(jìn)行合理的組織,在unittest中通過(guò)TestSuite類實(shí)現(xiàn):

    widgetTestSuite = unittest.TestSuite()
    widgetTestSuite.addTest(WidgetTestCase('test_default_size'))
    widgetTestSuite.addTest(WidgetTestCase('test_resize'))

為了方便測(cè)試,在每一個(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))

由于使用相似名字的測(cè)試函數(shù)來(lái)創(chuàng)建一個(gè)TestCase子類是非常通用的模式,unittest提供了一個(gè)TestLoader類可以自動(dòng)的創(chuàng)建測(cè)試套件并用獨(dú)立的測(cè)試進(jìn)行填充,比如:

    suite = unittest.TestLoader().loadTestsFromTestCase(WidgetTestCase)

這就創(chuàng)建了一個(gè)測(cè)試套件,將會(huì)執(zhí)行WidgetTestCase.test_default_size()和WidgetTestCase.test_resize。TestLoader將會(huì)自動(dòng)的識(shí)別以test_開(kāi)頭的測(cè)試方法。

各個(gè)測(cè)試的執(zhí)行順序是由測(cè)試的函數(shù)名按照字符串內(nèi)建順序執(zhí)行的。
測(cè)試套件本身也可以像測(cè)試用例一樣組織起來(lái):

    suite1 = module1.TheTestSuite()
    suite2 = module2.TheTestSuite()
    alltests = unittest.TestSuite([suite1, suite2])

我們可以將測(cè)試用例與測(cè)試套件的定義放在與測(cè)試代碼相同的模塊中,但將測(cè)試代碼放在單獨(dú)的模塊中又幾個(gè)好處:

  • 測(cè)試模塊可以獨(dú)立的通過(guò)命令行進(jìn)行執(zhí)行
  • 測(cè)試代碼可以更容易的與發(fā)布代碼分離
  • 沒(méi)有必要的情況下不必為了適應(yīng)被測(cè)試代碼而頻繁更改測(cè)試代碼
  • 測(cè)試的代碼更容易重構(gòu)
    。。。等等

跳過(guò)測(cè)試以及異常測(cè)試

unittest支持跳過(guò)單獨(dú)的測(cè)試方法甚至是整個(gè)測(cè)試類。也支持將一個(gè)測(cè)試標(biāo)記為“expected failure”。
跳過(guò)一個(gè)測(cè)試可以使用skip()描述符或者它的一個(gè)條件語(yǔ)句,最基本的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)

如果想跳過(guò)整個(gè)測(cè)試類,方法是和上面是一樣的。
期望失敗通過(guò)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)

被跳過(guò)的測(cè)試不會(huì)執(zhí)行setUp和tearDown,被跳過(guò)的類也不會(huì)執(zhí)行setUpClass和tearDownClass。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容