在Python中進(jìn)行自動化單元測試的教程
一、軟件測試
大型軟件系統(tǒng)的開發(fā)是一個很復(fù)雜的過程,其中因為人的因素而所產(chǎn)生的錯誤非常多,因此軟件在開發(fā)過程必須要有相應(yīng)的質(zhì)量保證活動,而軟件測試則是保證質(zhì)量的關(guān)鍵措施。正像軟件熵(software entropy)所描述的那樣:一個程序從設(shè)計很好的狀態(tài)開始,隨著新的功能不斷地加入,程序逐漸地失去了原有的結(jié)構(gòu),最終變成了一團(tuán)亂麻(其實最初的"很好的狀態(tài)"得加個問號)。測試的目的說起來其實很簡單也極具吸引力,那就是寫出高質(zhì)量的軟件并解決軟件熵這一問題。
可惜的是,軟件開發(fā)人員很少能在編碼的過程中就進(jìn)行軟件測試,大部分軟件項目都只在最終驗收時才進(jìn)行測試,有些項目甚至根本沒有測試計劃!隨著軟件質(zhì)量意識的增強,許多軟件開發(fā)組織開始轉(zhuǎn)向UML、CMM、RUP、XP等軟件工程方法,以期提高軟件質(zhì)量,并使軟件開發(fā)過程更加可控,好在這些方法對測試都提出了很嚴(yán)格的要求,從而使得測試在軟件開發(fā)過程的作用開始真正體現(xiàn)出來。
軟件測試作為一種系統(tǒng)工程,涉及到整個軟件開發(fā)過程的各個方面,需要管理人員、設(shè)計人員、開發(fā)人員和測試人員的共同努力。作為軟件開發(fā)過程中的主要力量,現(xiàn)今的程序員除了要編寫實現(xiàn)代碼外,還承擔(dān)著單元測試這一艱巨任務(wù),因此必須采用新的工作模式:
- 編寫和維護(hù)一套詳盡的單元測試用例;
- 先構(gòu)造單元測試和驗收測試用例,然后再編寫代碼;
- 根據(jù)構(gòu)造的測試用例來編寫代碼。
單元測試負(fù)責(zé)對最小的軟件設(shè)計單元(模塊)進(jìn)行驗證,它使用軟件設(shè)計文檔中對模塊的描述作為指南,對重要的程序分支進(jìn)行測試以發(fā)現(xiàn)模塊中的錯誤。由于軟件模塊并不是一個單獨的程序,為了進(jìn)行單元測試還必須編寫大量額外的代碼,從而無形中增加了開發(fā)人員的工作量,目前解決這一問題比較好的方法是使用測試框架。測試框架是在用XP方法進(jìn)行單元測試時的關(guān)鍵,尤其是在需要構(gòu)造大量測試用例時更是如此,因為如果完全依靠手工的方式來構(gòu)造和執(zhí)行這些測試,肯定會變成一個花費大量時間并且單調(diào)無味的工作,而測試框架則可以很好地解決這些問題。
使用Python語言的開發(fā)人員可以使用Steve Purcell編寫的PyUnit作為單元測試框架,通過將單元測試融合到PyUnit這一測試框架里,Python程序員可以更容易地增加、管理、執(zhí)行測試用例,并對測試結(jié)果進(jìn)行分析。此外,使用PyUnit還可以實現(xiàn)自動單元測試(回歸測試)。
二、規(guī)范Python單元測試
測試是一個貫穿于整個開發(fā)過程的連續(xù)過程,從某個意義上說,軟件開發(fā)的過程實際上就是測試過程。正如Martin Fowler所說的"在你不知道如何測試代碼之前,就不該編寫程序。而一旦你完成了程序,測試代碼也應(yīng)該完成。除非測試成功,你不能認(rèn)為你編寫出了可以工作的程序。"
測試最基本的原理就是比較預(yù)期結(jié)果是否與實際執(zhí)行結(jié)果相同,如果相同則測試成功,否則測試失敗。為了更好地理解PyUnit這一自動測試框架的作用,先來看一個簡單的例子,假設(shè)我們要對例1中的Widget類進(jìn)行測試:
例1. widget.py
# 將要被測試的類 class Widget: def __init__(self, size = (40, 40)): self._size = size def getSize(self): return self._size def resize(self, width, height): if width 0 or height < 0: raise ValueError, "illegal size" self._size = (width, height) def dispose(self): pass
采用手工方式進(jìn)行單元測試的Python程序員很可能會寫出類似例2的測試代碼來,
例2. manual.py
from widget import Widget # 執(zhí)行測試的類 class TestWidget: def testSize(self): expectedSize = (40, 40); widget = Widget() if widget.getSize() == expectedSize: print "test [Widget]: getSize works perfected!" else: print "test [Widget]: getSize doesn't work!" # 測試 if __name__ == '__main__': myTest = TestWidget() myTest.testSize()
稍一留心你不難發(fā)現(xiàn)這種手工測試方法存在許多問題。首先,測試程序的寫法沒有一定的規(guī)范可以遵循,十個程序員完全可能寫出十種不同的測試程序來,如果每個Python程序員都有自己不同的設(shè)計測試類的方法,光維護(hù)被測試的類就夠麻煩了,誰還顧得上維護(hù)測試類。其次,需要編寫大量的輔助代碼才能進(jìn)行單元測試,例1中用于測試的代碼甚至比被測試的代碼還要多,而這毫無疑問將增大Python程序員的工作量。
為了讓單元測試代碼能夠被測試和維護(hù)人員更容易地理解,最好的解決辦法是讓開發(fā)人員遵循一定的規(guī)范來編寫用于測試的代碼,具體到Python程序員來講,則是要采用PyUnit這一自動測試框架來構(gòu)造單元測試用例。目前PyUnit已經(jīng)得到了大多數(shù)Python開發(fā)人員的認(rèn)可,成了事實上的單元測試標(biāo)準(zhǔn)。如果采用PyUnit來進(jìn)行同樣的測試,則測試代碼將如例3所示:
例3. auto.py
from widget import Widget import unittest # 執(zhí)行測試的類 class WidgetTestCase(unittest.TestCase): def setUp(self): self.widget = Widget() def tearDown(self): self.widget = None def testSize(self): self.assertEqual(self.widget.getSize(), (40, 40)) # 構(gòu)造測試集 def suite(): suite = unittest.TestSuite() suite.addTest(WidgetTestCase("testSize")) return suite # 測試 if __name__ == "__main__": unittest.main(defaultTest = 'suite')
在采用PyUnit這一單元測試框架后,用于測試的代碼做了相應(yīng)的改動:
- 用import語句引入unittest模塊。
- 讓所有執(zhí)行測試的類都繼承于TestCase類,可以將TestCase看成是對特定類進(jìn)行測試的方法的集合。
- 在setUp()方法中進(jìn)行測試前的初始化工作,并在tearDown()方法中執(zhí)行測試后的清除工作,setUp()和tearDown()都是TestCase類中定義的方法。
- 在testSize()中調(diào)用assertEqual()方法,對Widget類中g(shù)etSize()方法的返回值和預(yù)期值進(jìn)行比較,確保兩者是相等的,assertEqual()也是TestCase類中定義的方法。
- 提供名為suite()的全局方法,PyUnit在執(zhí)行測試的過程調(diào)用suit()方法來確定有多少個測試用例需要被執(zhí)行,可以將TestSuite看成是包含所有測試用例的一個容器。
雖然看起來有點復(fù)雜,但PyUnit使得所有的Python程序員都可以使用同樣的單元測試方法,測試過程不再是雜亂無章的了,而是在同一規(guī)范指導(dǎo)下進(jìn)行的有序行為,這就是使用PyUnit這一自動單元測試框架所帶來的最大好處。
三、自動測試框架PyUnit
在對軟件測試?yán)碚摵蚉yUnit有了一個大致了解之后,下面輔以具體的實例介紹Python程序員如何借助PyUnit來進(jìn)行單元測試。所有的代碼均在Python 2.2.2下調(diào)試通過,操作系統(tǒng)使用的是Red Hat Linux 9。
3.1 安裝
在Python中進(jìn)行單元測試時需要用到PyUnit模塊,Python 2.1及其以后的版本都將PyUnit作為一個標(biāo)準(zhǔn)模塊,但如果你使用的是較老版本的Python,那就要自已動手安裝了。在PyUnit的網(wǎng)站(http://sourceforge.net/projects/pyunit)上可以下載到PyUnit最新的源碼包,此處使用的是pyunit-1.4.1.tar.gz。
在下載好PyUnit軟件包后,執(zhí)行下面的命令對其進(jìn)行解壓縮:
[root@gary source]# tar xzvf pyunit-1.4.1.tar.gz
要在Python程序中使用PyUnit模塊,最簡單的辦法是確保PyUni軟件包中的文件unittest.py和unittestgui.py都包含在Python的搜索路徑中,這既可以通過直接設(shè)置PYTHONPATH環(huán)境變量來實現(xiàn),也可以執(zhí)行以下的命令來將它們復(fù)制到Python的當(dāng)前搜索路徑中:
[root@gary source]# cd pyunit-1.4.1 [root@gary pyunit-1.4.1]# python setup.py install
3.2 測試用例TestCase
軟件測試中最基本的組成單元是測試用例(test case),PyUnit使用TestCase類來表示測試用例,并要求所有用于執(zhí)行測試的類都必須從該類繼承。TestCase子類實現(xiàn)的測試代碼應(yīng)該是自包含(self contained)的,也就是說測試用例既可以單獨運行,也可以和其它測試用例構(gòu)成集合共同運行。
TestCase在PyUnit測試框架中被視為測試單元的運行實體,Python程序員可以通過它派生自定義的測試過程與方法(測試單元),利用Command和Composite設(shè)計模式,多個TestCase還可以組合成測試用例集合。PyUnit測試框架在運行一個測試用例時,TestCase子類定義的setUp()、runTest()和tearDown()方法被依次執(zhí)行,最簡單的測試用例只需覆蓋runTest()方法來執(zhí)行特定的測試代碼就可以了,如例4所示:
例4. static_single.py
import unittest # 執(zhí)行測試的類 class WidgetTestCase(unittest.TestCase): def runTest(self): widget = Widget() self.assertEqual(widget.getSize(), (40, 40))
而要在PyUnit測試框架中構(gòu)造上述WidgetTestCase類的一個實例,應(yīng)該不帶任何參數(shù)調(diào)用其構(gòu)造函數(shù):
testCase = WidgetTestCase()
一個測試用例通常只對軟件模塊中的一個方法進(jìn)行測試,采用覆蓋runTest()方法來構(gòu)造測試用例在PyUnit中稱為靜態(tài)方法,如果要對同一個軟件模塊中的多個方法進(jìn)行測試,通常需要構(gòu)造多個執(zhí)行測試的類,如例5所示:
例5. static_multi.py
import unittest # 測試getSize()方法的測試用例 class WidgetSizeTestCase(unittest.TestCase): def runTest(self): widget = Widget() self.assertEqual(widget.getSize(), (40, 40)) # 測試resize()方法的測試用例 class WidgetResizeTestCase(unittest.TestCase): def runTest(self): widget = Widget() widget.resize(100, 100) self.assertEqual(widget.getSize(), (100, 100))
采用靜態(tài)方法,Python程序員不得不為每個要測試的方法編寫一個測試類(該類通過覆蓋runTest()方法來執(zhí)行測試),并在每一個測試類中生成一個待測試的對象。在為同一個軟件模塊編寫測試用例時,很多時候待測對象有著相同的初始狀態(tài),因此采用上述方法的Python程序員不得不在每個測試類中為待測對象進(jìn)行同樣的初始化工作,而這往往是一項費時且枯燥的工作。
一種更好的解決辦法是采用PyUnit提供的動態(tài)方法,只編寫一個測試類來完成對整個軟件模塊的測試,這樣對象的初始化工作可以在setUp()方法中完成,而資源的釋放則可以在tearDown()方法中完成,如例6所示:
例6. dynamic.py
import unittest # 執(zhí)行測試的類 class WidgetTestCase(unittest.TestCase): def setUp(self): self.widget = Widget() def tearDown(self): self.widget.dispose() self.widget = None def testSize(self): self.assertEqual(self.widget.getSize(), (40, 40)) def testResize(self): self.widget.resize(100, 100) self.assertEqual(self.widget.getSize(), (100, 100))
采用動態(tài)方法最大的好處是測試類的結(jié)構(gòu)非常好,用于測試一個軟件模塊的所有代碼都可以在同一個類中實現(xiàn)。動態(tài)方法不再覆蓋runTest()方法,而是為測試類編寫多個測試方法(按習(xí)慣這些方法通常以test開頭),在創(chuàng)建TestCase子類的實例時必須給出測試方法的名稱,來為PyUnit測試框架指明運行該測試用例時究竟應(yīng)該調(diào)用測試類中的哪個方法:
sizeTestCase = WidgetTestCase("testSize") resizeTestCase = WidgetTestCase("testResize")
3.3 測試用例集TestSuite
完整的單元測試很少只執(zhí)行一個測試用例,開發(fā)人員通常都需要編寫多個測試用例才能對某一軟件功能進(jìn)行比較完整的測試,這些相關(guān)的測試用例稱為一個測試用例集,在PyUnit中是用TestSuite類來表示的。
在創(chuàng)建了一些TestCase子類的實例作為測試用例之后,下一步要做的工作就是用TestSuit類來組織它們。PyUnit測試框架允許Python程序員在單元測試代碼中定義一個名為suite()的全局函數(shù),并將其作為整個單元測試的入口,PyUnit通過調(diào)用它來完成整個測試過程。
def suite(): suite = unittest.TestSuite() suite.addTest(WidgetTestCase("testSize")) suite.addTest(WidgetTestCase("testResize")) return suite
也可以直接定義一個TestSuite的子類,并在其初始化方法(__init__)中完成所有測試用例的添加:
class WidgetTestSuite(unittest.TestSuite): def __init__(self): unittest.TestSuite.__init__(self, map(WidgetTestCase, ("testSize", "testResize")))
這樣只需要在suite()方法中返回該類的一個實例就可以了:
def suite(): return WidgetTestSuite()
如果用于測試的類中所有的測試方法都以test開,Python程序員甚至可以用PyUnit模塊提供的makeSuite()方法來構(gòu)造一個TestSuite:
def suite(): return unittest.makeSuite(WidgetTestCase, "test")
在PyUnit測試框架中,TestSuite類可以看成是TestCase類的一個容器,用來對多個測試用例進(jìn)行組織,這樣多個測試用例可以自動在一次測試中全部完成。事實上,TestSuite除了可以包含TestCase外,也可以包含TestSuite,從而可以構(gòu)成一個更加龐大的測試用例集:
suite1 = mysuite1.TheTestSuite() suite2 = mysuite2.TheTestSuite() alltests = unittest.TestSuite((suite1, suite2))
3.4 實施測試
編寫測試用例(TestCase)并將它們組織成測試用例集(TestSuite)的最終目的只有一個:實施測試并獲得最終結(jié)果。PyUnit使用TestRunner類作為測試用例的基本執(zhí)行環(huán)境,來驅(qū)動整個單元測試過程。Python開發(fā)人員在進(jìn)行單元測試時一般不直接使用TestRunner類,而是使用其子類TextTestRunner來完成測試,并將測試結(jié)果以文本方式顯示出來:
runner = unittest.TextTestRunner() runner.run(suite)
使用TestRunner來實施測試的例子如例7所示,
例7. text_runner.py
from widget import Widget import unittest # 執(zhí)行測試的類 class WidgetTestCase(unittest.TestCase): def setUp(self): self.widget = Widget() def tearDown(self): self.widget.dispose() self.widget = None def testSize(self): self.assertEqual(self.widget.getSize(), (40, 40)) def testResize(self): self.widget.resize(100, 100) self.assertEqual(self.widget.getSize(), (100, 100)) # 測試 if __name__ == "__main__": # 構(gòu)造測試集 suite = unittest.TestSuite() suite.addTest(WidgetTestCase("testSize")) suite.addTest(WidgetTestCase("testResize")) # 執(zhí)行測試 runner = unittest.TextTestRunner() runner.run(suite)
要執(zhí)行該單元測試,可以使用如下命令:
[xiaowp@gary code]$ python text_runner.py
運行結(jié)果應(yīng)該如下所示,表明執(zhí)行了2個測試用例,并且兩者都通過了測試:
.. ---------------------------------------------------------------------- Ran 2 tests in 0.000s OK
如果對數(shù)據(jù)進(jìn)行修改,模擬出錯的情形,將會得到如下結(jié)果:
.F ========================================== FAIL: testResize (__main__.WidgetTestCase) ---------------------------------------------------------------------- Traceback (most recent call last): File "text_runner.py", line 15, in testResize self.assertEqual(self.widget.getSize(), (200, 100)) File "/usr/lib/python2.2/unittest.py", line 286, in failUnlessEqual raise self.failureException, \ AssertionError: (100, 100) != (200, 100) ---------------------------------------------------------------------- Ran 2 tests in 0.001s FAILED (failures=1)
默認(rèn)情況下,TextTestRunner將結(jié)果輸出到sys.stderr上,但如果在創(chuàng)建TextTestRunner類實例時將一個文件對象傳遞給了構(gòu)造函數(shù),則輸出結(jié)果將被重定向到該文件中。在Python的交互環(huán)境中驅(qū)動單元測試時,使用TextTestRunner類是一個不錯的選擇。
PyUnit模塊中定義了一個名為main的全局方法,使用它可以很方便地將一個單元測試模塊變成可以直接運行的測試腳本,main()方法使用TestLoader類來搜索所有包含在該模塊中的測試方法,并自動執(zhí)行它們。如果Python程序員能夠按照約定(以test開頭)來命名所有的測試方法,那就只需要在測試模塊的最后加入如下幾行代碼即可:
if __name__ == "__main__": unittest.main()
使用main()方法來實施測試的例子如例8所示,
例8. main_runner.py
from widget import Widget import unittest # 執(zhí)行測試的類 class WidgetTestCase(unittest.TestCase): def setUp(self): self.widget = Widget() def tearDown(self): self.widget.dispose() self.widget = None def testSize(self): self.assertEqual(self.widget.getSize(), (40, 40)) def testResize(self): self.widget.resize(100, 100) self.assertEqual(self.widget.getSize(), (100, 100)) # 測試 if __name__ == "__main__": unittest.main()
要執(zhí)行該單元測試,可以使用如下命令:
[xiaowp@gary code]$ python main_runner.py
測試類WidgetTestCase中的所有測試方法都將被自動執(zhí)行,但如果只想執(zhí)行testSize()方法,可以使用如下命令:
[xiaowp@gary code]$ python main_runner.py WidgetTestCase.testSize
如果在單元測試腳本中定義了TestSuite,還可以指定要運行的測試集。使用-h參數(shù)可以查看運行該腳本所有可能用到的參數(shù):
[xiaowp@gary code]$ python main_runner.py -h
為了使單元測試更具親合力,PyUnit軟件包中還提供了一個圖形界面測試腳本unittestgui.py,將其復(fù)制到當(dāng)前目錄后,可以執(zhí)行下面的命令來啟動該測試工具,對main_runner.py腳本中的所有測試用例進(jìn)行測試:
[xiaowp@gary code]$ python unittestgui.py main_runner
該測試工具動行時的界面如圖1所示:
圖1. 圖形測試工具
單擊Start按鈕可以開始執(zhí)行所有測試用例,測試結(jié)果將如圖2所示:
圖2 測試結(jié)果
使用圖形界面可以更好地進(jìn)行單元測試,查詢測試結(jié)果也更加方便。PyUnit對于沒有通過的測試會進(jìn)行區(qū)分,指明它是失?。╢ailure)還是錯誤(error),失敗是被assert類方法(如assertEqual)檢查到的預(yù)期結(jié)果,而錯誤則是由意外情況所引起的。
四、小結(jié)
測試是保證軟件質(zhì)量的關(guān)鍵,新的軟件開發(fā)方法要求程序員在編寫代碼前先編寫測試用例,并在軟件開發(fā)過程中不斷地進(jìn)行單元測試,從而最大限度地減少缺陷(Bug)的產(chǎn)生。軟件單元測試是XP方法的基石,測試框架為程序員進(jìn)行單元測試提供了統(tǒng)一的規(guī)范,Python程序員可以使用PyUnit作為軟件開發(fā)過程中的自動單元測試框架。
- Python基于Hypothesis測試庫生成測試數(shù)據(jù)
- Python Unittest自動化單元測試框架詳解
- 全面介紹python中很常用的單元測試框架unitest
- python單元測試框架pytest的使用示例
- Python編寫單元測試代碼實例
- Python unittest單元測試openpyxl實現(xiàn)過程解析
- Python unittest單元測試框架實現(xiàn)參數(shù)化
- Python unittest單元測試框架及斷言方法
- python3.6編寫的單元測試示例
- Python Django框架單元測試之文件上傳測試示例
- python 如何用 Hypothesis 來自動化單元測試
相關(guān)文章
PyTorch一小時掌握之神經(jīng)網(wǎng)絡(luò)分類篇
這篇文章主要介紹了PyTorch一小時掌握之神經(jīng)網(wǎng)絡(luò)分類篇,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2021-09-09Tensorflow中k.gradients()和tf.stop_gradient()用法說明
這篇文章主要介紹了Tensorflow中k.gradients()和tf.stop_gradient()用法說明,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-06-06python爬蟲搭配起B(yǎng)ilibili唧唧的流程分析
這篇文章主要介紹了python爬蟲搭配起B(yǎng)ilibili唧唧的流程分析,本文通過圖文并茂的形式給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-12-12關(guān)于Python數(shù)據(jù)處理中的None、NULL和NaN的理解與應(yīng)用
這篇文章主要介紹了關(guān)于Python數(shù)據(jù)處理中的None、NULL和NaN的理解與應(yīng)用,None表示空值,一個特殊Python對象,None的類型是NoneType,需要的朋友可以參考下2023-08-08使用selenium模擬動態(tài)登錄百度頁面的實現(xiàn)
本文主要介紹了使用selenium模擬動態(tài)登錄百度頁面,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-05-05Python使用import導(dǎo)入本地腳本及導(dǎo)入模塊的技巧總結(jié)
這篇文章主要介紹了Python使用import導(dǎo)入本地腳本及導(dǎo)入模塊的技巧,結(jié)合實例形式總結(jié)分析了Python使用import導(dǎo)入本地腳本及導(dǎo)入模塊的使用方法及相關(guān)操作注意事項,需要的朋友可以參考下2019-08-08PyTorch實現(xiàn)MNIST數(shù)據(jù)集手寫數(shù)字識別詳情
這篇文章主要介紹了PyTorch實現(xiàn)MNIST數(shù)據(jù)集手寫數(shù)字識別詳情,文章圍繞主題展開詳細(xì)的內(nèi)容戒殺,具有一定的參考價值,需要的朋友可以參考一下2022-09-09