目錄
概況
Python單元測試框架(The Python unit testing framework),簡稱為PyUnit, 是Kent Beck和Erich Gamma這兩位聰明的家伙所設計的 JUnit 的Python版本。 而JUnit又是Kent設計的Smalltalk測試框架的Java版本。它們都是各自語言的標准測試框架。
此文檔僅闡述針對Python的單元測試PyUnit的設計與使用。如需單元測試框架基本設計的背景 信息,請查閱Kent的原始文章"Simple Smalltalk Testing: With Patterns"。
自從 Python 2.1 版本后,PyUnit成為 Python標准庫的一部分。
以下內容默認您已經了解Python。我覺得Python 非常簡單易學而且讓人欲罷不能。
系統要求
PyUnit可以在Python 1.5.2及更高版本上運行。
作者已經在Linux(Redhat 6.0和6.1以及Debian Potato)和Python 1.5.2, 2.0和2.1上對PyUnit 進行了測試。而且PyUnit已知可以在其它操作系統平台上工作,如Windows和Mac。如果您在 任何系統平台或Python版本中遇到麻煩,請讓我知道。
如需了解在JPython和Jython中使用PyUnit的細節,請閱讀 在JPython和Jython中使用PyUnit部分。
使用PyUnit構建自己的測試
安裝
編寫測試所需的類可以在“unittest”模塊中找到。此模塊是Python 2.1和更高版本的標准 庫的一部分。如果你在使用更早版本的Python,你應該從單獨的PyUnit發布中獲得此模塊。
為使此模塊能在你的代碼中正常工作,你只需確保包含“unittest.py”文件的目錄 在你的Python搜索路徑中。為此,你可以修改環境變量“$PYTHONPATH”或將此文件 放入當前Python搜索路徑中的某一個目錄中,比如在Redhat Linux系統中的 /usr/lib/python1.5/site-packages目錄。
注意,你只有完成此項工作才能運行PyUnit所自帶的例子,除非你將“unittest.py”復制到 例子目錄。
測試用例介紹
單元測試是由一些測試用例(Test Cases)構建組成的。測試用例是被設置用來檢測正確性的 單獨的場景。在PyUnit中,unittest模塊中的TestCase 類代表測試用例。
TestCase類的實例是可以完全運行測試方法和可選的設置 (set-up)以及清除(tidy-up)代碼的對象。
TestCase實例的測試代碼必須是自包含的,換言之,它 可以單獨運行或與其它任意數量的測試用例共同運行。
創建一個簡單測試用例
通過覆蓋runTest方法即可得到最簡單的測試用例子類以運行 一些測試代碼:
import unittest
class DefaultWidgetSizeTestCase(unittest.TestCase):
def runTest(self):
widget = Widget("The widget")
assert widget.size() == (50,50), 'incorrect default size'
注意:為進行測試,我們只是使用了Python內建的“assert”語句。如果在測試用例 運行時斷言(assertion)為假,AssertionError異常會被拋出,並且 測試框架會認為測試用例失敗。其它非“assert”檢查所拋出的異常會被測試框架認為是“errors”。 (參見"更多關於測試條件")
運行測試用例的方法會在后面介紹。現在我們只是通過調用無參數的構造器(constructor) 來創建一個測試用例的實例:
testCase = DefaultWidgetSizeTestCase()
復用設置代碼:創建固件
現在,這樣的測試用例數量巨大且它們的設置需要很多重復性工作。在上面的測試用例中, 如若在100個Widget測試用例的每一個子類中都創建一個“Widget”,那會導致難看的重復。
幸運的是,我們可以將這些設置代碼提取出來並放置在一個叫做setUp的 鈎子方法(hook method)中。測試框架會在運行測試時自動調用此方法:
import unittest
class SimpleWidgetTestCase(unittest.TestCase):
def setUp(self):
self.widget = Widget("The widget")
class DefaultWidgetSizeTestCase(SimpleWidgetTestCase):
def runTest(self):
assert self.widget.size() == (50,50), 'incorrect default size'
class WidgetResizeTestCase(SimpleWidgetTestCase):
def runTest(self):
self.widget.resize(100,150)
assert self.widget.size() == (100,150), \
'wrong size after resize'
如果setUp方法在測試運行時拋出異常,框架會認為測試遇到了錯誤並且 runTest不會被執行。
類似的,我們也可以提供一個tearDown方法來完成在runTest運行之后的清理工作:
import unittest
class SimpleWidgetTestCase(unittest.TestCase):
def setUp(self):
self.widget = Widget("The widget")
def tearDown(self):
self.widget.dispose()
self.widget = None
如果setUp執行成功, 那么無論runTest是否成功,tearDown方法都將被執行。
Such a working environment for the testing code is termed a fixture. 這個測試代碼的運行環境被稱為固件(fixture,譯者注:此為暫定譯法,意為固定的構件或方法)。
包含多個測試方法的測試用例類
很多小型測試用例經常會使用相同的固件。在這個用例中,我們最終從SimpleWidgetTestCase繼承產生很多僅包含一個方法的類,如 DefaultWidgetSizeTestCase。這是很耗時且不被鼓勵的,因此,沿用JUnit的風格,PyUnit提供了一個更簡便的方法:
import unittest
class WidgetTestCase(unittest.TestCase):
def setUp(self):
self.widget = Widget("The widget")
def tearDown(self):
self.widget.dispose()
self.widget = None
def testDefaultSize(self):
assert self.widget.size() == (50,50), 'incorrect default size'
def testResize(self):
self.widget.resize(100,150)
assert self.widget.size() == (100,150), \
'wrong size after resize'
在這個用例中,我們沒有提供runTest方法,而是兩個不同的測試方法。類實例將創建和銷毀各自的self.widget並運行某一個test方法。 當創建類實例時,我們必須通過向構造器傳遞方法的名稱來指明哪個測試方法將被運行:
defaultSizeTestCase = WidgetTestCase("testDefaultSize")
resizeTestCase = WidgetTestCase("testResize")
將測試用例聚合成測試套件
測試用例實例可以根據它們所測試的特性組合到一起。PyUnit為此提供了一個機制叫做”測試套件“(test suite)。它由unittest模塊中的TestSuite類表示:
widgetTestSuite = unittest.TestSuite()
widgetTestSuite.addTest(WidgetTestCase("testDefaultSize"))
widgetTestSuite.addTest(WidgetTestCase("testResize"))
我們稍后會看到,在每個測試模塊中提供一個返回已創建測試套件的可調用對象,會是一個使測試更加便捷的好方法:
def suite():
suite = unittest.TestSuite()
suite.addTest(WidgetTestCase("testDefaultSize"))
suite.addTest(WidgetTestCase("testResize"))
return suite
甚至可寫成:
class WidgetTestSuite(unittest.TestSuite):
def __init__(self):
unittest.TestSuite.__init__(self,map(WidgetTestCase,
("testDefaultSize",
"testResize")))
(誠然,第二種方法不是為膽小者准備的)
因為創建一個包含很多相似名稱的測試方法的TestCase子類是一種很常見的模式,所以unittest模塊提供一個便捷方法,makeSuite,來 創建一個由測試用例類內所有測試用例組成的測試套件:
suite = unittest.makeSuite(WidgetTestCase,'test')
需要注意的是,當使用makeSuite方法時,測試套件運行每個測試用例的順序是由測試方法名根據Python內建函數cmp所排序的順序而決定的。
嵌套測試套件
我們經常希望將一些測試套件組合在一起來一次性的測試整個系統。這很簡單,因為多個TestSuite可以被加入進另一個TestSuite,就如同 多個TestCase被加進一個TestSuite中一樣:
suite1 = module1.TheTestSuite()
suite2 = module2.TheTestSuite()
alltests = unittest.TestSuite((suite1, suite2))
在發布的軟件包中的“examples”目錄中,"alltests.py”提供了使用嵌套測試套件的例子
測試代碼放置位置
你可以將測試用例定義與被測試代碼置於同一個模塊中(例如“widget.py”),但是將測試代碼放置在單獨的模塊中(如“widgettests.py”)會有一些優勢:
- 測試模塊可以從命令行單獨執行
- 測試代碼可以方便地從發布代碼中分離
- 少了在缺乏充足理由的情況下為適應被測試代碼而更改測試代碼的誘惑
- 相對於被測試代碼,測試代碼不應該被頻繁的修改
- 被測試代碼可以更方法的進行重構
- 既然C語言代碼的測試應該置於單獨的模塊,那何不保持這個一致性呢?
- 如果測試策略改變,也無需修改被測試源代碼
交互式運行測試
我們編寫測試的主要目的是運行它們並檢查我們的軟件是否工作正常。測試框架使用“TestRunner”類來為運行測試提供環境。最常用的TestRunner是TextTestRunner, 它可以以文字方式運行測試並報告結果:
runner = unittest.TextTestRunner()
runner.run(widgetTestSuite)
TextTestRunner默認將輸出發送到sys.stderr,但是你可以通過向它的構造器傳遞一個不同的類似文件(file-object)對象來改變默認方式。
如需在Python解釋器會話中運行測試,這樣使用TextTestRunner是一個理想的方法。
從命令行運行測試
unittest模塊包含一個main方法,可以方便地將測試模塊轉變為可以運行測試的腳本。main 使用unittest.TestLoader類來自動查找和加載模塊內測試用例。
因此,如果你之前已經使用test*慣例對測試方法進行命名,那么你就可以將以下代碼插入測試模塊的結尾:
if __name__ == "__main__":
unittest.main()
這樣,當你從命令行執行你的測試模塊時,其所包含的所有測試都將被運行。使用“-h”選項運行模塊可以查看所有可用的選項。
如需從命令行運行任意測試,你可以將unittest模塊作為腳本運行,並將所需執行的測試套件中的測試用例名稱作為參數傳遞給此腳本:
% python unittest.py widgettests.WidgetTestSuite
or
% python unittest.py widgettests.makeWidgetTestSuite
你還可以在命令行指明特定的測試(方法)來執行。如要運行“listtests”模塊中的TestCase類的子類 'ListTestCase'(參見發布軟件包中的“examples”子目錄), 你可以執行以下命令:
% python unittest.py listtests.ListTestCase.testAppend
“testAppend”是測試用例實例將要執行的測試方法的名稱。你可以執行以下代碼來創建ListTestCase類實例並執行其所包含的所有“test*”測試方法:
% python unittest.py listtests.ListTestCase
在用戶界面窗口運行測試
你還可以使用圖形化窗口運行你的測試。它是用Tkinter編寫的。在多數平台上,這個窗口工具與Python是捆綁在一起發布的。它看上去和JUnit窗口很相似。
你只需運行以下命令來使用測試運行窗口:
% python unittestgui.py
or
% python unittestgui.py widgettests.WidgetTestSuite
這里需要注意的是,所輸入的測試名稱必須是一個可以返回TestCase或TestSuite類實例的對象名稱,不可以是事先創建好的測試名稱, 因為每個測試必須在每次運行是重新創建。
使用窗口測試會因為更新那些窗口而帶來額外的時間開銷。在我系統上,每一千個測試,它會多花七秒鍾。你的消耗可能會不同。
為測試編寫文檔
通常當測試運行時,TestRunner將顯示其名稱。這個名稱是由測試用例類名和所運行的測試方法名組成的。
但是如果你為測試方法提供了doc-string,則當測試運行時,doc-string的第一行將被顯示出來。這為編寫測試文檔提供了一個很便捷的機制:
class WidgetTestCase(unittest.TestCase):
def testDefaultSize(self):
"""Check that widgets are created with correct default size"""
assert self.widget.size() == (50,50), 'incorrect default size'
更多關於測試條件
我之前建議過應使用Python內建斷言機制來檢查測試用例中的條件,而不應使用自己編寫的替代品,因為assert更簡單,簡明且為大家所熟悉。
但是值得注意的是,如果在運行測試的同時Python優化選項被打開(生成“.pyo"字節碼文件),那么assert語句將會被跳過,使得測試用例變得無用。
我為那些需要使用Python優化選項的用戶編寫了一個assert_方法並添加進TestCase類內。它的功能和內建的assert相同且 不會被優化刪除,但是使用較麻煩且所輸出錯誤信息幫助較小:
def runTest(self):
self.assert_(self.widget.size() == (100,100), "size is wrong")
我還在TestCase類中提供了failIf和failUnless兩個方法:
def runTest(self):
self.failIf(self.widget.size() <> (100,100))
測試方法還可以通過調用fail方法使得測試立即失敗:
def runTest(self):
...
if not hasattr(something, "blah"):
self.fail("blah missing")
# or just 'self.fail()'
測試相等性
最常用的斷言是測試相等性。如果斷言失敗,開發者通常希望看到實際錯誤值。
TestCase包含一對方法assertEqual和assertNotEqual用於此目的(如果你喜歡,你還可以使用別名:failUnlessEqual 和 failIfEqual):
def testSomething(self):
self.widget.resize(100,100)
self.assertEqual(self.widget.size, (100,100))
測試異常
測試經常希望檢查在某個環境中是否出現異常。如果期待的異常沒有拋出,測試將失敗。這很容易做到:
def runTest(self):
try:
self.widget.resize(-1,-1)
except ValueError:
pass
else:
fail("expected a ValueError")
通常,預期異常源(譯者注:將拋出異常的代碼)是一個可調用對象;為此,TestCase有一個assertRaises方法。此方法的前兩個參數是應該出現在“except”語句中的異常和可調用對象。剩余的參數是應該傳遞給可調用對象的參數。
def runTest(self):
self.assertRaises(ValueError, self.widget.resize, -1, -1)
通過PyUnit復用舊測試代碼
一些用戶希望將已有的測試代碼不需轉變為TestCase子類而直接從PyUnit中運行。
為此,PyUnit提供了一個FunctionTestCase類。這個TestCase子類可以用來包裝已有測試函數。設置和清理函數也可以選擇性地被包裝。
對於以下測試函數:
def testSomething():
something = makeSomething()
assert something.name is not None
...
我們可以創建一個等同的測試用例實例:
testcase = unittest.FunctionTestCase(testSomething)
如果有附加的設置和清理方法需要由測試用例調用,可以如下操作:
testcase = unittest.FunctionTestCase(testSomething,
setUp=makeSomethingDB,
tearDown=deleteSomethingDB)
在JPython和Jython中使用PyUnit
雖然PyUnit主要是為“C” Python所編寫,你仍然可以用Jython編寫PyUnit測試,來測試你的Java或Jython軟件。這比用Jython編寫JUnit測試更可取。PyUnit也可以正確的與Jython前期版本,Jython 1.0和1.1協同工作。
當然,Java不包含TK GUI接口,所以PyUnit的基於TKinter的GUI是不能在Jython下工作的,但是基於文本的接口是可以正常工作的。
要在Jython中使用PyUnit的文本接口,只需簡單的將標准C Python庫模塊文件‘traceback.py', 'linecache.py', 'stat.py' 和 'getopt.py'復制到可以被JPython引用到的位置上。你可以在任何C Python發布版中找到這些文件。(這是針對C Python 1.5.x版本的標准庫,可能對其它版本Python不適用)
現在你完全可以像在C Python中那樣編寫你的PyUnit測試了。
注意事項
斷言
參見 "更多關於測試條件" 部分所述注意事項。
內存使用
當異常在測試套件運行過程中被拋出時,因此產生的追溯(traceback)對象將被保存,以使失敗信息可以在測試運行結束后被格式化輸出。除了簡便性,這樣做的另一個優點就是未來的GUI TestRunner可以在后期查看保存在追溯對象中的本地和全局變量。
一個可能的副作用就是,當運行一個失敗頻率很高的測試套件時,為保存所有這些追溯對象而需要的內存使用量將成為一個問題。當然,如果很多測試是失敗的,內存的消耗也只是你的問題中最微不足道的一個。
使用條款
你可以依據Python所使用的自由條款來自由的使用,更改和重新發布此軟件。我只要求我的名字,email地址和項目URL保留在代碼和隨行文檔中,給予我作為原作者的尊重。
我編寫此軟件的初衷是為改進世界上軟件質量而貢獻微薄之力;我不求金錢回報。(這不是說我不歡迎贊助)
未來計划
一個關鍵的未來計划是將TK GUI和IDLE IDE整合在一起,歡迎加入!
除此之外,我沒有要擴展此模塊功能的龐大計划。我使PyUnit盡可能的簡單(希望不能再簡單了)因為我相信一些常用的輔助性的模塊,比如日志文件比較,最好還是由測試編寫者自行編寫。
更新與社區
新聞,更新以及更多信息可以在項目網站獲得。
歡迎各種評論,建議和錯誤報告;只需給我發送電子郵件或者這個非常小量的郵件列表並發表你的評論。現在有大量的PyUnit使用者,他們都有智慧與大家分享。
鳴謝
Many thanks to Guido and his disciples for the Python language. In tribute, I have written the following haiku (or 'pyku', if you will):
Guido van Rossum
'Gawky Dutchman' gave birth to
Beautiful Python
I gratefully acknowledge the work of Kent Beck and Erich Gamma for their work on JUnit, which made the design of PyUnit a no-brainer.
Thanks also to Tim Voght; I discovered after I had implemented PyUnit that he had also implemented a 'pyunit' module as part of his 'PyWiki' WikiWikiWeb clone. He graciously gave me the go-ahead to submit my version to the community at large.
Many thanks to those who have written to me with suggestions and questions. I've tried to add appropriate credits in the CHANGES file in the download package.
Particular thanks to J¨¦rôme Marant, who packaged PyUnit for Debian.
相關信息
- PyUnit網站
- Python網站
- "Simple Smalltalk Testing: With Patterns" 作者: Kent Beck
- JUnit網站
- XProgramming.com -- 極限編程主頁
- ExtremeProgramming 於 WikiWikiWeb
- PyWiki -- Python WikiWikiWeb 由 Tim Voght復制
