簡介: 軟體的測試是一件非常乏味的事情,在測試別人編寫的軟體時尤其如此,程式員通常都只對編寫代碼感興趣,而不喜歡文檔編寫和軟體測試這類"沒有創新"的工作。既然如此,為什麼不讓程式員在編寫軟體時自己加入一些用於測試的代碼,使測試過程自動化呢?在軟體工程中,這一技術稱為自動單元測試,本文介紹在用Python開發軟體時如何?這一目標。
一、軟體測試
大型軟體系統的開發是一個很複雜的過程,其中因為人的因素而所產生的錯誤非常多,因此軟體在開發過程必須要有相應的品質保證活動,而軟體測試則是保證品質的關鍵措施。正像軟體熵(software entropy)所描述的那樣:一個程式從設計很好的狀態開始,隨著新的功能不斷地加入,程式逐漸地失去了原有的結構,最終變成了一團亂麻(其實最初的"很好的狀態"得加個問號)。測試的目的說起來其實很簡單也極具吸引力,那就是寫出高品質的軟體並解決軟體熵這一問題。
可惜的是,軟體開發人員很少能在編碼的過程中就進行軟體測試,大部分軟體項目都只在最終驗收時才進行測試,有些項目甚至根本沒有測試計劃!隨著軟體品質意識的增強,許多軟體開發組織開始轉向UML、CMM、RUP、XP等軟體工程方法,以期提高軟體品質,並使軟體開發過程更加可控,好在這些方法對測試都提出了很嚴格的要求,從而使得測試在軟體開發過程的作用開始真正體現出來。
軟體測試作為一種系統工程,涉及到整個軟體開發過程的各個方面,需要管理員、設計人員、開發人員和測試人員的共同努力。作為軟體開發過程中的主要力量,現今的程式員除了要編寫實現代碼外,還承擔著單元測試這一艱巨任務,因此必須採用新的工作模式:
- 編寫和維護一套詳盡的單元測試用例;
- 先構造單元測試和驗收測試用例,然後再編寫代碼;
- 根據構造的測試案例來編寫代碼。
單元測試負責對最小的軟體設計單元(模組)進行驗證,它使用軟體設計文檔中對模組的描述作為指南,對重要的程式分支進行測試以發現模組中的錯誤。由於軟體模組並不是一個單獨的程式,為了進行單元測試還必須編寫大量額外的代碼,從而無形中增加了開發人員的工作量,目前解決這一問題比較好的方法是使用測試架構。測試架構是在用XP方法進行單元測試時的關鍵,尤其是在需要構造大量測試案例時更是如此,因為如果完全依靠手工的方式來構造和執行這些測試,肯定會變成一個花費大量時間並且單調無味的工作,而測試架構則可以很好地解決這些問題。
使用Python語言的開發人員可以使用Steve Purcell編寫的PyUnit作為單元測試架構,通過將單元測試融合到PyUnit這一測試架構裡,Python程式員可以更容易地增加、管理、執行測試案例,並對測試結果進行分析。此外,使用PyUnit還可以實現自動單元測試(迴歸測試)。
回頁首
二、規範Python單元測試
測試是一個貫穿於整個開發過程的連續過程,從某個意義上說,軟體開發的過程實際上就是測試過程。正如Martin Fowler所說的"在你不知道如何測試代碼之前,就不該編寫程式。而一旦你完成了程式,測試代碼也應該完成。除非測試成功,你不能認為你編寫出了可以工作的程式。"
測試最基本的原理就是比較預期結果是否與實際執行結果相同,如果相同則測試成功,否則測試失敗。為了更好地理解PyUnit這一自動化的測試架構的作用,先來看一個簡單的例子,假設我們要對例1中的Widget類進行測試:
例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 |
採用手工方式進行單元測試的Python程式員很可能會寫出類似例2的測試代碼來,
例2. manual.pyfrom widget import Widget# 執行測試的類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() |
稍一留心你不難發現這種手工測試方法存在許多問題。首先,測試程式的寫法沒有一定的規範可以遵循,十個程式員完全可能寫出十種不同的測試程式來,如果每個Python程式員都有自己不同的設計測試類別的方法,光維護被測試的類就夠麻煩了,誰還顧得上維護測試類別。其次,需要編寫大量的輔助代碼才能進行單元測試,例1中用於測試的代碼甚至比被測試的代碼還要多,而這毫無疑問將增大Python程式員的工作量。
為了讓單元測試代碼能夠被測試和維護人員更容易地理解,最好的解決辦法是讓開發人員遵循一定的規範來編寫用於測試的代碼,具體到Python程式員來講,則是要採用PyUnit這一自動化的測試架構來構造單元測試用例。目前PyUnit已經得到了大多數Python開發人員的認可,成了事實上的單元測試標準。如果採用PyUnit來進行同樣的測試,則測試代碼將如例3所示:
例3. auto.pyfrom widget import Widgetimport unittest# 執行測試的類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))# 構造測試集def suite(): suite = unittest.TestSuite() suite.addTest(WidgetTestCase("testSize")) return suite# 測試if __name__ == "__main__": unittest.main(defaultTest = 'suite') |
在採用PyUnit這一單元測試架構後,用於測試的代碼做了相應的改動:
- 用import語句引入unittest模組。
- 讓所有執行測試的類都繼承於TestCase類,可以將TestCase看成是對特定類進行測試的方法的集合。
- 在setUp()方法中進行測試前的初始化工作,並在tearDown()方法中執行測試後的清除工作,setUp()和tearDown()都是TestCase類中定義的方法。
- 在testSize()中調用assertEqual()方法,對Widget類中getSize()方法的傳回值和預期值進行比較,確保兩者是相等的,assertEqual()也是TestCase類中定義的方法。
- 提供名為suite()的全域方法,PyUnit在執行測試的程序呼叫suit()方法來確定有多少個測試案例需要被執行,可以將TestSuite看成是包含所有測試案例的一個容器。
雖然看起來有點複雜,但PyUnit使得所有的Python程式員都可以使用同樣的單元測試方法,測試過程不再是雜亂無章的了,而是在同一規範指導下進行的有序行為,這就是使用PyUnit這一自動單元測試架構所帶來的最大好處。
回頁首
三、自動化的測試架構PyUnit
在對軟體測試理論和PyUnit有了一個大致瞭解之後,下面輔以具體的執行個體介紹Python程式員如何藉助PyUnit來進行單元測試。所有的代碼均在Python 2.2.2下調試通過,作業系統使用的是Red Hat Linux 9。
回頁首
3.1 安裝
在Python中進行單元測試時需要用到PyUnit模組,Python 2.1及其以後的版本都將PyUnit作為一個標準模組,但如果你使用的是較老版本的Python,那就要自已動手安裝了。在PyUnit的網站(http://sourceforge.net/projects/pyunit)上可以下載到PyUnit最新的源碼包,此處使用的是pyunit-1.4.1.tar.gz。
在下載好PyUnit軟體包後,執行下面的命令對其進行解壓縮:
[root@gary source]# tar xzvf pyunit-1.4.1.tar.gz |
要在Python程式中使用PyUnit模組,最簡單的辦法是確保PyUni軟體包中的檔案unittest.py和unittestgui.py都包含在Python的搜尋路徑中,這既可以通過直接設定PYTHONPATH環境變數來實現,也可以執行以下的命令來將它們複製到Python的當前搜尋路徑中:
[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類來表示測試案例,並要求所有用於執行測試的類都必須從該類繼承。TestCase子類實現的測試代碼應該是自包含(self contained)的,也就是說測試案例既可以單獨運行,也可以和其它測試案例構成集合共同運行。
TestCase在PyUnit測試架構中被視為測試單元的運行實體,Python程式員可以通過它派生自訂的測試過程與方法(測試單元),利用Command和Composite設計模式,多個TestCase還可以組合成測試案例集合。PyUnit測試架構在運行一個測試案例時,TestCase子類定義的setUp()、runTest()和tearDown()方法被依次執行,最簡單的測試案例只需覆蓋runTest()方法來執行特定的測試代碼就可以了,如例4所示:
例4. static_single.pyimport unittest# 執行測試的類class WidgetTestCase(unittest.TestCase): def runTest(self): widget = Widget() self.assertEqual(widget.getSize(), (40, 40)) |
而要在PyUnit測試架構中構造上述WidgetTestCase類的一個執行個體,應該不帶任何參數調用其建構函式:
testCase = WidgetTestCase() |
一個測試案例通常只對軟體模組中的一個方法進行測試,採用覆蓋runTest()方法來構造測試案例在PyUnit中稱為靜態方法,如果要對同一個軟體模組中的多個方法進行測試,通常需要構造多個執行測試的類,如例5所示:
例5. static_multi.pyimport 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)) |
採用靜態方法,Python程式員不得不為每個要測試的方法編寫一個測試類別(該類通過覆蓋runTest()方法來執行測試),並在每一個測試類別中產生一個待測試的對象。在為同一個軟體模組編寫測試案例時,很多時候待測對象有著相同的初始狀態,因此採用上述方法的Python程式員不得不在每個測試類別中為待測對象進行同樣的初始化工作,而這往往是一項費時且枯燥的工作。
一種更好的解決辦法是採用PyUnit提供的動態方法,只編寫一個測試類別來完成對整個軟體模組的測試,這樣對象的初始化工作可以在setUp()方法中完成,而資源的釋放則可以在tearDown()方法中完成,如例6所示:
例6. dynamic.pyimport unittest# 執行測試的類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)) |
採用動態方法最大的好處是測試類別的結構非常好,用於測試一個軟體模組的所有代碼都可以在同一個類中實現。動態方法不再覆蓋runTest()方法,而是為測試類別編寫多個測試方法(按習慣這些方法通常以test開頭),在建立TestCase子類的執行個體時必須給出測試方法的名稱,來為PyUnit測試架構指明運行該測試案例時究竟應該調用測試類別中的哪個方法:
sizeTestCase = WidgetTestCase("testSize")resizeTestCase = WidgetTestCase("testResize") |
3.3 測試案例集TestSuite
完整的單元測試很少只執行一個測試案例,開發人員通常都需要編寫多個測試案例才能對某一軟體功能進行比較完整的測試,這些相關的測試案例稱為一個測試案例集,在PyUnit中是用TestSuite類來表示的。
在建立了一些TestCase子類的執行個體作為測試案例之後,下一步要做的工作就是用TestSuit類來組織它們。PyUnit測試架構允許Python程式員在單元測試代碼中定義一個名為suite()的全域函數,並將其作為整個單元測試的入口,PyUnit通過調用它來完成整個測試過程。
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()方法來構造一個TestSuite:
def suite(): return unittest.makeSuite(WidgetTestCase, "test") |
在PyUnit測試架構中,TestSuite類可以看成是TestCase類的一個容器,用來對多個測試案例進行組織,這樣多個測試案例可以自動在一次測試中全部完成。事實上,TestSuite除了可以包含TestCase外,也可以包含TestSuite,從而可以構成一個更加龐大的測試案例集:
suite1 = mysuite1.TheTestSuite()suite2 = mysuite2.TheTestSuite()alltests = unittest.TestSuite((suite1, suite2)) |
3.4 實施測試
編寫測試案例(TestCase)並將它們組織成測試案例集(TestSuite)的最終目的只有一個:實施測試並獲得最終結果。PyUnit使用TestRunner類作為測試案例的基本執行環境,來驅動整個單元測試過程。Python開發人員在進行單元測試時一般不直接使用TestRunner類,而是使用其子類TextTestRunner來完成測試,並將測試結果以文本方式顯示出來:
runner = unittest.TextTestRunner()runner.run(suite) |
使用TestRunner來實施測試的例子如例7所示,
例7. text_runner.pyfrom widget import Widgetimport unittest# 執行測試的類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__": # 構造測試集 suite = unittest.TestSuite() suite.addTest(WidgetTestCase("testSize")) suite.addTest(WidgetTestCase("testResize")) # 執行測試 runner = unittest.TextTestRunner() runner.run(suite) |
要執行該單元測試,可以使用如下命令:
[xiaowp@gary code]$ python text_runner.py |
運行結果應該如下所示,表明執行了2個測試案例,並且兩者都通過了測試:
..----------------------------------------------------------------------Ran 2 tests in 0.000sOK |
如果對資料進行修改,類比出錯的情形,將會得到如下結果:
.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.001sFAILED (failures=1) |
預設情況下,TextTestRunner將結果輸出到sys.stderr上,但如果在建立TextTestRunner類執行個體時將一個檔案對象傳遞給了建構函式,則輸出結果將被重新導向到該檔案中。在Python的互動環境中驅動單元測試時,使用TextTestRunner類是一個不錯的選擇。
PyUnit模組中定義了一個名為main的全域方法,使用它可以很方便地將一個單元測試模組變成可以直接啟動並執行測試指令碼,main()方法使用TestLoader類來搜尋所有包含在該模組中的測試方法,並自動執行它們。如果Python程式員能夠按照約定(以test開頭)來命名所有的測試方法,那就只需要在測試模組的最後加入如下幾行代碼即可:
if __name__ == "__main__": unittest.main() |
使用main()方法來實施測試的例子如例8所示,
例8. main_runner.pyfrom widget import Widgetimport unittest# 執行測試的類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() |
要執行該單元測試,可以使用如下命令:
[xiaowp@gary code]$ python main_runner.py |
測試類別WidgetTestCase中的所有測試方法都將被自動執行,但如果只想執行testSize()方法,可以使用如下命令:
[xiaowp@gary code]$ python main_runner.py WidgetTestCase.testSize |
如果在單元測試指令碼中定義了TestSuite,還可以指定要啟動並執行測試集。使用-h參數可以查看運行該指令碼所有可能用到的參數:
[xiaowp@gary code]$ python main_runner.py -h |
為了使單元測試更具親合力,PyUnit軟體包中還提供了一個圖形介面測試指令碼unittestgui.py,將其複製到目前的目錄後,可以執行下面的命令來啟動該測試載入器,對main_runner.py指令碼中的所有測試案例進行測試:
[xiaowp@gary code]$ python unittestgui.py main_runner |
該測試載入器動行時的介面1所示:
圖1. 圖形測試載入器
單擊Start按鈕可以開始執行所有測試案例,測試結果將2所示:
圖2 測試結果
使用圖形介面可以更好地進行單元測試,查詢測試結果也更加方便。PyUnit對於沒有通過的測試會進行區分,指明它是失敗(failure)還是錯誤(error),失敗是被assert類方法(如assertEqual)檢查到的預期結果,而錯誤則是由意外情況所引起的。
回頁首
四、小結
測試是保證軟體品質的關鍵,新的軟體開發方法要求程式員在編寫代碼前先編寫測試案例,並在軟體開發過程中不斷地進行單元測試,從而最大限度地減少缺陷(Bug)的產生。軟體單元測試是XP方法的基石,測試架構為程式員進行單元測試提供了統一的規範,Python程式員可以使用PyUnit作為軟體開發過程中的自動單元測試架構。
參考資料
1. 本文代碼下載: 代碼
2. 可以從Python網站( http://www.python.org)著手瞭解所有關於Python的知識。
3. 在PyUnit網站( http://sourceforge.net/projects/pyunit)可以下載到最新的PyUnit軟體包,以及詳細的使用者手冊。
4. Kent Beck的文章"Simple Smalltalk Testing: With Patterns"( http://www.xprogramming.com/testfram.htm)從設計模式的角度討論了測試架構的基本原理。
5. 在極限編程網站( http://www.xprogramming.com)上可以瞭解XP方法的基本原理和方法。
轉載聲明: 本文轉自 http://www.ibm.com/developerworks/cn/linux/l-pyunit/index.html