Python中的測試模塊unittest和doctest的使用教程
我要坦白一點。盡管我是一個應用相當廣泛的公共域 Python 庫的創(chuàng)造者,但在我的模塊中引入的單元測試是非常不系統(tǒng)的。實際上,那些測試大部分 是包括在 gnosis.xml.pickle 的 Gnosis Utilities 中的,并由該子軟件包(subpackage)的貢獻者所編寫。我還發(fā)現(xiàn),我下載的絕大多數(shù)第三方 Python 包都缺少完備的單元測試集。
不僅如此,Gnosis Utilities 中現(xiàn)有的測試也受困于另一個缺陷:您經常需要在極其大量的細節(jié)中去推定期望的輸出,以確定測試的成敗。測試實際上 -- 在很多情況下 -- 更像是使用庫的某些部分的小實用工具。這些測試(或實用工具)支持來自任意數(shù)據(jù)源(類型正確)的輸入和/或描述性數(shù)據(jù)格式的輸出。實際上,當您需要調試一些細微的錯誤時,這些測試實用工具更有用。但是對于庫版本間變化的自解釋的完整性檢查(sanity checks)來說,這些類測試就不能勝任了。
在這一期文章中,我嘗試使用 Python 標準庫模塊 doctest 和 unittest 來改進我的實用工具集中的測試,并帶領您與我一起體驗(并指出一些最好的方法)。
腳本 gnosis/xml/objectify/test/test_basic.py 給出了一個關于當前測試的缺點及解決方案的典型示例。下面是該腳本的最新版本:
清單 1. test_basic.py
"Read and print and objectified XML file" import sys from gnosis.xml.objectify import XML_Objectify, pyobj_printer if len(sys.argv) > 1: for filename in sys.argv[1:]: for parser in ('DOM','EXPAT'): try: xml_obj = XML_Objectify(filename, parser=parser) py_obj = xml_obj.make_instance() print pyobj_printer(py_obj).encode('UTF-8') sys.stderr.write("++ SUCCESS (using "+parser+")\n") print "="*50 except: sys.stderr.write("++ FAILED (using "+parser+")\n") print "="*50 else: print "Please specify one or more XML files to Objectify."
實用工具函數(shù) pyobj_printer() 生成了任意 Python 對象(具體說是這樣一個對象,它既沒有用到 gnosis.xml.objectify 的任何其他實用工具,也沒有用到 Gnosis Utilities 中的 任何其他東西)的一個 非-XML 表示。在以后的版本中,我將可能會把這個函數(shù)移到 Gnosis 包內的其他地方。無論如何, pyobj_printer() 使用各種類-Python 的縮進和符號來描述對象和它們的屬性(類似于 pprint ,但是擴展了實例,而不僅限于擴展內置的數(shù)據(jù)類型)。
如果一些特別的 XML 可能不能正確被地“對象化(objectified)”, test_basic.py 腳本會提供一個很好的調試工具 -- 您可以可視化地查看結果對象的屬性和值。此外,如果您重定向了 STDOUT,您可以查看 STDERR 上的簡單消息,如這個例子中:
清單 2. 分析 STDERR 結果消息
$ python test_basic.py testns.xml > /dev/null ++ SUCCESS (using DOM) ++ FAILED (using EXPAT)
不過,上面運行的例子中對成功或失敗的界定很不明顯:成功只是意味著沒有出現(xiàn)異常,而不表示(重定向的)輸出 正確。
使用 doctest
doctest 模塊讓您可以在文檔字符串(docstrings)內嵌入注釋以顯示各種語句的期望行為,尤其是函數(shù)和方法的結果。這樣做很像是讓文檔字符串看起來如同一個交互式 shell 會話;完成這一任務的一個簡單方法是,從一個 Python 交互式 shell 中(或者從 Idel、PythonWin、MacPython 或者其他帶有交互式會話的 IDE 中)拷貝-粘貼。這一改進的 test_basic.py 腳本舉例說明了自診斷功能的添加:
清單 3. 具有自診斷功能的 test_basic.py 腳本
import sys from gnosis.xml.objectify import XML_Objectify, pyobj_printer, EXPAT, DOM LF = "\n" def show(xml_src, parser): """Self test using simple or user-specified XML data >>> xml = '''<?xml version="1.0"?> ... <!DOCTYPE Spam SYSTEM "spam.dtd" > ... <Spam> ... <Eggs>Some text about eggs.</Eggs> ... <MoreSpam>Ode to Spam</MoreSpam> ... </Spam>''' >>> squeeze = lambda s: s.replace(LF*2,LF).strip() >>> print squeeze(show(xml,DOM)[0]) -----* _XO_Spam *----- {Eggs} PCDATA=Some text about eggs. {MoreSpam} PCDATA=Ode to Spam >>> print squeeze(show(xml,EXPAT)[0]) -----* _XO_Spam *----- {Eggs} PCDATA=Some text about eggs. {MoreSpam} PCDATA=Ode to Spam PCDATA= """ try: xml_obj = XML_Objectify(xml_src, parser=parser) py_obj = xml_obj.make_instance() return (pyobj_printer(py_obj).encode('UTF-8'), "++ SUCCESS (using "+parser+")\n") except: return ("","++ FAILED (using "+parser+")\n") if __name__ == "__main__": if len(sys.argv)==1 or sys.argv[1]=="-v": import doctest, test_basic doctest.testmod(test_basic) elif sys.argv[1] in ('-h','-help','--help'): print "You may specify XML files to objectify instead of self-test" print "(Use '-v' for verbose output, otherwise no message means success)" else: for filename in sys.argv[1:]: for parser in (DOM, EXPAT): output, message = show(filename, parser) print output sys.stderr.write(message) print "="*50
注意,我在經過改進(和擴展)的測試腳本中放入了 main 代碼塊,這樣,如果您在命令行中指定了 XML 文件,腳本將繼續(xù)執(zhí)行以前的行為。這樣就讓您可以繼續(xù)分析測試用例以外其他的 XML,并只著眼于結果 -- 或者找出 gnosis.xml.objectify 所做事情中的錯誤,或者只是理解其目的。按標準的方式,您可以使用 -h 或 --help 參數(shù)來獲得用法的說明。
當不帶任何參數(shù)(或者帶有只被 doctest 使用的 -v 參數(shù))運行 test_basic.py 時,就會發(fā)現(xiàn)有趣的新功能。在這個例子中,我們在模塊/腳本自身上運行 doctest -- 您可以看到,實際上我們將 test_basic 導入到腳本自己的名稱空間中,這樣我們可以簡單地導入其他希望要測試的模塊。 doctest.testmod() 函數(shù)去遍歷模塊本身、它的函數(shù)以及它的類中的所有文檔字符串,以找出所有類似交互式 shell 會話的內容;在這個例子中,會在 show() 函數(shù)中找到這樣一個會話。
show() 的文檔字符串舉例說明了在設計好的 doctest 會話過程中的幾個小“陷阱(gotchas)”。不幸的是, doctest 在解析顯式會話時,將空行作為會話結束來處理 -- 所以,像 pyobj_printer() 的返回值這樣的輸出需要加一些保護(be munged slightly)以進行測試。最簡單的途徑是使用文檔字符串本身所定義的像 squeeze() 這樣的函數(shù)(它只是除去緊跟在后面的換行)。此外,由于文檔字符串畢竟是字符串換碼(escape),所以 \n 這樣的序列被擴展,這樣使得在代碼示例 內部對換行進行換碼稍微有一些混亂。您可以使用 \\n ,不過我發(fā)現(xiàn)對 LF 的定義解決了這些問題。
在 show() 的文檔字符串中定義的自測試所做的不僅是確保不發(fā)生異常(對照于最初的測試腳本)。為正確的“對象化(objectification)”至少要檢查一個簡單的 XML 文檔。當然,仍然有可能不能正確地處理一些其他的 XML 文檔 -- 例如,上面我們試過的名稱空間 XML 文檔 testns.xml 遇到了 EXPAT 解析器失敗。由 doctest處理的文檔字符串 可能會在其內部包含回溯(traceback),但是在特別的情況下,更好的方法是使用 unittest 。
使用 unittest
另一個包含在 gnosis.xml.objectify 中的測試是 test_expat.py 。創(chuàng)建這一測試的主要原因僅在于,使用 EXPAT 解析器的子軟件包用戶常常需要調用一個特別的設置函數(shù)來啟用有名稱空間的 XML 文檔的處理(這個實際情況是演化來的而不是設計如此,并且以后可能會改變)。老的測試會試圖不借助設置去打印對象,如果發(fā)生異常則捕獲之,然后如果需要的話借助設置再去打?。ú⒔o出一個關于所發(fā)生事情的消息)。
而如果使用 test_basic.py , test_expat.py 工具讓您可以分析 gnosis.xml.objectify 如何去描述一個新奇的 XML 文檔。但是與以前一樣,有很多我們可能想去驗證的具體行為。 test_expat.py 的一個增強的、擴展的版本使用 unittest 來分析各種動作執(zhí)行時發(fā)生的事情,包括持有特定條件或(近似)等式的斷言,或出現(xiàn)期望的某些異常??匆豢矗?/p>
清單 4. 自診斷的 test_expat.py 腳本
"Objectify using Expat parser, namespace setup where needed" import unittest, sys, cStringIO from os.path import isfile from gnosis.xml.objectify import make_instance, config_nspace_sep,\ XML_Objectify BASIC, NS = 'test.xml','testns.xml' class Prerequisite(unittest.TestCase): def testHaveLibrary(self): "Import the gnosis.xml.objectify library" import gnosis.xml.objectify def testHaveFiles(self): "Check for sample XML files, NS and BASIC" self.failUnless(isfile(BASIC)) self.failUnless(isfile(NS)) class ExpatTest(unittest.TestCase): def setUp(self): self.orig_nspace = XML_Objectify.expat_kwargs.get('nspace_sep','') def testNoNamespace(self): "Objectify namespace-free XML document" o = make_instance(BASIC) def testNamespaceFailure(self): "Raise SyntaxError on non-setup namespace XML" self.assertRaises(SyntaxError, make_instance, NS) def testNamespaceSuccess(self): "Sucessfully objectify NS after setup" config_nspace_sep(None) o = make_instance(NS) def testNspaceBasic(self): "Successfully objectify BASIC despite extra setup" config_nspace_sep(None) o = make_instance(BASIC) def tearDown(self): XML_Objectify.expat_kwargs['nspace_sep'] = self.orig_nspace if __name__ == '__main__': if len(sys.argv) == 1: unittest.main() elif sys.argv[1] in ('-q','--quiet'): suite = unittest.TestSuite() suite.addTest(unittest.makeSuite(Prerequisite)) suite.addTest(unittest.makeSuite(ExpatTest)) out = cStringIO.StringIO() results = unittest.TextTestRunner(stream=out).run(suite) if not results.wasSuccessful(): for failure in results.failures: print "FAIL:", failure[0] for error in results.errors: print "ERROR:", error[0] elif sys.argv[1].startswith('-'): # pass args to unittest unittest.main() else: from gnosis.xml.objectify import pyobj_printer as show config_nspace_sep(None) for fname in sys.argv[1:]: print show(make_instance(fname)).encode('UTF-8')
使用 unittest 為較簡單的 doctest 方式增添了相當多的能力。我們可以將我們的測試分為幾個類,每一個類都繼承自 unittest.TestCase 。在每一個測試類內部,每一個名稱以“.test”開始的方法都被認為是另一個測試。為 ExpatTest 定義的兩個額外的類很有趣:在每次使用類執(zhí)行測試前運行 .setUp() ,測試結束時運行 .tearDown() (不管測試是成功、失敗還是出現(xiàn)錯誤)。在我們上面的例子中,我們?yōu)閷S玫?expat_kwargs 字典做了一點簿記以確保每個測試獨立地運行。
順便提一下,失敗(failure)和錯誤(error)之間的區(qū)別很重要。一個測試可能會因為一些具體的斷言無效而失?。〝嘌苑椒ɑ蛘咭浴?fail”開頭,或者以“.assert”開頭)。在某種意義上,失敗是期望中的 -- 最起碼從某種意義上我們已經具體分析過。另一方面,錯誤是意外的問題 -- 因為我們事先不知道哪里會出錯,我們需要分析實際測試運行中的回溯來診斷這種問題。不過,我們可以設計讓失敗給出診斷錯誤的提示。例如,如果 Prerequisite.haveFiles() 失敗,將在一些 TestExpat 測試中出現(xiàn)錯誤;如果前者是成功的,您將不得不到其他地方去查找錯誤的根源。
在 unittest.TestCase 的繼承類中,具體的測試方法中可能會包括一些 .assert...() 或者 .fail...() 方法,但也可能只是具有一系列我們相信應該會成功執(zhí)行的動作。如果測試方法沒有按預期運行,我們將得到一個錯誤(以及描述這個錯誤的回溯)。
test_expat.py 中的 _main_ 程序塊也值得察看。在最簡單的情況下,我們可以只使用 unittest.main() 來運行測試用例,這將斷定哪些需要運行。使用這種方式時, unittest 模塊將接受一個 -v 選項以給出更詳細的輸出。根據(jù)指定的文件名,在執(zhí)行了名稱空間設置后,我們打印出指定的 XML 文件的表示,從而大致上保持了對此工具稍老版本的向后兼容。
_main_ 中最有趣的分支是期待 -q 或 --quiet 標簽的那個分支。如您將期望的,除非發(fā)生失敗或錯誤,否則這個分支將是靜默的(quiet,即盡量減少輸出)。不僅如此,由于它是靜默的,它只會為每個問題顯示一行關于失敗/錯誤位置的報告,而不是整個診斷回溯。除了對靜默輸出風格的直接利用以外,這個分支還舉例說明了相對于測試套件的自定義測試以及對結果報告的控制。稍微有些長的 unittest.TextTestRunner() 的默認輸出被定向到 StringIO out -- 如果您想查看它,歡迎您到 out.getvalue() 去查找。不過, result 對象讓我們對全面成功進行測試,如果不是完全成功還可以讓我們處理失敗和錯誤。顯然,由于它們是變量中的值,您可以輕松地將 result 對象的內容記錄入日志,或者在 GUI 中顯示,不管怎么樣,不是僅僅打印到 STDOUT。
組合測試
可能 unittest 框架最好的特性是讓您可以輕松地組合包含不同模塊的測試。實際上,如果使用 Python 2.3+,您甚至可以將 doctest 測試轉化為 unittest 套件。讓我們將到目前為止所創(chuàng)建的測試組合到一個腳本 test_all.py 中(誠然,說它是我們目前為止所做的測試有些夸張):
清單 5. test_all.py 組合了單元測試
"Combine tests for gnosis.xml.objectify package (req 2.3+)" import unittest, doctest, test_basic, test_expat suite = doctest.DocTestSuite(test_basic) suite.addTest(unittest.makeSuite(test_expat.Prerequisite)) suite.addTest(unittest.makeSuite(test_expat.ExpatTest)) unittest.TextTestRunner(verbosity=2).run(suite)
由于 test_expat.py 只是包含測試類,所以它們可以容易地添加到本地的測試套件中。 doctest.DocTestSuite() 函數(shù)執(zhí)行文檔字符串測試的轉換。讓我們來看看 test_all.py 運行時會做什么:
清單 6. 來自 test_all.py 的成功輸出
$ python2.3 test_all.py doctest of test_basic.show ... ok Check for sample XML files, NS and BASIC ... ok Import the gnosis.xml.objectify library ... ok Raise SyntaxError on non-setup namespace XML ... ok Sucessfully objectify NS after setup ... ok Objectify namespace-free XML document ... ok Successfully objectify BASIC despite extra setup ... ok ---------------------------------------------------------------------- Ran 7 tests in 0.052s OK
注意對執(zhí)行的測試的描述:在使用 unittest 測試方法的情況下,他們的描述來自于相應的 docstring 函數(shù)。如果您沒有指定文檔字符串,類和方法名被用作最合適的描述。來看一下如果一些測試失敗時我們會得到什么,同樣有趣(為本文去掉了回溯細節(jié)):
清單 7. 當一些測試失敗時的結果
$ mv testns.xml testns.xml# && python2.3 test_all.py 2>&1 | head -7 doctest of test_basic.show ... ok Check for sample XML files, NS and BASIC ... FAIL Import the gnosis.xml.objectify library ... ok Raise SyntaxError on non-setup namespace XML ... ERROR Sucessfully objectify NS after setup ... ERROR Objectify namespace-free XML document ... ok Successfully objectify BASIC despite extra setup ... ok
隨便提及,這個失敗寫到 STDERR 的最后一行是“FAILED (failures=1, errors=2)”,如果您需要的話這是一個很好的總結(相對于成功時最終的“OK”)。
從這里開始
本文向您介紹了 unittest 和 doctest 的一些典型用法,它們已經改進了我自己的軟件中的測試。閱讀 Python 文檔,以深入了解可用于測試套件、測試用例和測試結果的全部范圍的方法。它們全部都遵循例子中所描述的模式。
讓自己遵從 Python 的標準測試模塊規(guī)定的方法學是良好的軟件實踐。測試驅動(test-driven)的開發(fā)在很多軟件周期中都很流行;不過,顯然 Python 是一門適合于測試驅動模型的語言。而且,如果只是考慮軟件包更可能按計劃工作,一個軟件包或庫如果伴隨有一組周全的測試,會比缺乏這些測試的軟件包或庫對用戶更為有用。
相關文章
python開發(fā)之tkinter實現(xiàn)圖形隨鼠標移動的方法
這篇文章主要介紹了python開發(fā)之tkinter實現(xiàn)圖形隨鼠標移動的方法,涉及Python基于tkinter繪圖的相關實現(xiàn)技巧,具有一定參考借鑒價值,需要的朋友可以參考下2015-11-11Python時間管理黑科技之datetime函數(shù)詳解
在Python中,datetime模塊是處理日期和時間的標準庫,它提供了一系列功能強大的函數(shù)和類,用于處理日期、時間、時間間隔等,本文將深入探討datetime模塊的使用方法,感興趣的可以了解下2023-08-08