Python中的測試模塊unittest和doctest的使用教程
我要坦白一點(diǎn)。盡管我是一個(gè)應(yīng)用相當(dāng)廣泛的公共域 Python 庫的創(chuàng)造者,但在我的模塊中引入的單元測試是非常不系統(tǒng)的。實(shí)際上,那些測試大部分 是包括在 gnosis.xml.pickle 的 Gnosis Utilities 中的,并由該子軟件包(subpackage)的貢獻(xiàn)者所編寫。我還發(fā)現(xiàn),我下載的絕大多數(shù)第三方 Python 包都缺少完備的單元測試集。
不僅如此,Gnosis Utilities 中現(xiàn)有的測試也受困于另一個(gè)缺陷:您經(jīng)常需要在極其大量的細(xì)節(jié)中去推定期望的輸出,以確定測試的成敗。測試實(shí)際上 -- 在很多情況下 -- 更像是使用庫的某些部分的小實(shí)用工具。這些測試(或?qū)嵱霉ぞ撸┲С謥碜匀我鈹?shù)據(jù)源(類型正確)的輸入和/或描述性數(shù)據(jù)格式的輸出。實(shí)際上,當(dāng)您需要調(diào)試一些細(xì)微的錯(cuò)誤時(shí),這些測試實(shí)用工具更有用。但是對于庫版本間變化的自解釋的完整性檢查(sanity checks)來說,這些類測試就不能勝任了。
在這一期文章中,我嘗試使用 Python 標(biāo)準(zhǔn)庫模塊 doctest 和 unittest 來改進(jìn)我的實(shí)用工具集中的測試,并帶領(lǐng)您與我一起體驗(yàn)(并指出一些最好的方法)。
腳本 gnosis/xml/objectify/test/test_basic.py 給出了一個(gè)關(guān)于當(dāng)前測試的缺點(diǎn)及解決方案的典型示例。下面是該腳本的最新版本:
清單 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í)用工具函數(shù) pyobj_printer() 生成了任意 Python 對象(具體說是這樣一個(gè)對象,它既沒有用到 gnosis.xml.objectify 的任何其他實(shí)用工具,也沒有用到 Gnosis Utilities 中的 任何其他東西)的一個(gè) 非-XML 表示。在以后的版本中,我將可能會(huì)把這個(gè)函數(shù)移到 Gnosis 包內(nèi)的其他地方。無論如何, pyobj_printer() 使用各種類-Python 的縮進(jìn)和符號(hào)來描述對象和它們的屬性(類似于 pprint ,但是擴(kuò)展了實(shí)例,而不僅限于擴(kuò)展內(nèi)置的數(shù)據(jù)類型)。
如果一些特別的 XML 可能不能正確被地“對象化(objectified)”, test_basic.py 腳本會(huì)提供一個(gè)很好的調(diào)試工具 -- 您可以可視化地查看結(jié)果對象的屬性和值。此外,如果您重定向了 STDOUT,您可以查看 STDERR 上的簡單消息,如這個(gè)例子中:
清單 2. 分析 STDERR 結(jié)果消息
$ python test_basic.py testns.xml > /dev/null ++ SUCCESS (using DOM) ++ FAILED (using EXPAT)
不過,上面運(yùn)行的例子中對成功或失敗的界定很不明顯:成功只是意味著沒有出現(xiàn)異常,而不表示(重定向的)輸出 正確。
使用 doctest
doctest 模塊讓您可以在文檔字符串(docstrings)內(nèi)嵌入注釋以顯示各種語句的期望行為,尤其是函數(shù)和方法的結(jié)果。這樣做很像是讓文檔字符串看起來如同一個(gè)交互式 shell 會(huì)話;完成這一任務(wù)的一個(gè)簡單方法是,從一個(gè) Python 交互式 shell 中(或者從 Idel、PythonWin、MacPython 或者其他帶有交互式會(huì)話的 IDE 中)拷貝-粘貼。這一改進(jìn)的 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
注意,我在經(jīng)過改進(jìn)(和擴(kuò)展)的測試腳本中放入了 main 代碼塊,這樣,如果您在命令行中指定了 XML 文件,腳本將繼續(xù)執(zhí)行以前的行為。這樣就讓您可以繼續(xù)分析測試用例以外其他的 XML,并只著眼于結(jié)果 -- 或者找出 gnosis.xml.objectify 所做事情中的錯(cuò)誤,或者只是理解其目的。按標(biāo)準(zhǔn)的方式,您可以使用 -h 或 --help 參數(shù)來獲得用法的說明。
當(dāng)不帶任何參數(shù)(或者帶有只被 doctest 使用的 -v 參數(shù))運(yùn)行 test_basic.py 時(shí),就會(huì)發(fā)現(xiàn)有趣的新功能。在這個(gè)例子中,我們在模塊/腳本自身上運(yùn)行 doctest -- 您可以看到,實(shí)際上我們將 test_basic 導(dǎo)入到腳本自己的名稱空間中,這樣我們可以簡單地導(dǎo)入其他希望要測試的模塊。 doctest.testmod() 函數(shù)去遍歷模塊本身、它的函數(shù)以及它的類中的所有文檔字符串,以找出所有類似交互式 shell 會(huì)話的內(nèi)容;在這個(gè)例子中,會(huì)在 show() 函數(shù)中找到這樣一個(gè)會(huì)話。
show() 的文檔字符串舉例說明了在設(shè)計(jì)好的 doctest 會(huì)話過程中的幾個(gè)小“陷阱(gotchas)”。不幸的是, doctest 在解析顯式會(huì)話時(shí),將空行作為會(huì)話結(jié)束來處理 -- 所以,像 pyobj_printer() 的返回值這樣的輸出需要加一些保護(hù)(be munged slightly)以進(jìn)行測試。最簡單的途徑是使用文檔字符串本身所定義的像 squeeze() 這樣的函數(shù)(它只是除去緊跟在后面的換行)。此外,由于文檔字符串畢竟是字符串換碼(escape),所以 \n 這樣的序列被擴(kuò)展,這樣使得在代碼示例 內(nèi)部對換行進(jìn)行換碼稍微有一些混亂。您可以使用 \\n ,不過我發(fā)現(xiàn)對 LF 的定義解決了這些問題。
在 show() 的文檔字符串中定義的自測試所做的不僅是確保不發(fā)生異常(對照于最初的測試腳本)。為正確的“對象化(objectification)”至少要檢查一個(gè)簡單的 XML 文檔。當(dāng)然,仍然有可能不能正確地處理一些其他的 XML 文檔 -- 例如,上面我們試過的名稱空間 XML 文檔 testns.xml 遇到了 EXPAT 解析器失敗。由 doctest處理的文檔字符串 可能會(huì)在其內(nèi)部包含回溯(traceback),但是在特別的情況下,更好的方法是使用 unittest 。
使用 unittest
另一個(gè)包含在 gnosis.xml.objectify 中的測試是 test_expat.py 。創(chuàng)建這一測試的主要原因僅在于,使用 EXPAT 解析器的子軟件包用戶常常需要調(diào)用一個(gè)特別的設(shè)置函數(shù)來啟用有名稱空間的 XML 文檔的處理(這個(gè)實(shí)際情況是演化來的而不是設(shè)計(jì)如此,并且以后可能會(huì)改變)。老的測試會(huì)試圖不借助設(shè)置去打印對象,如果發(fā)生異常則捕獲之,然后如果需要的話借助設(shè)置再去打?。ú⒔o出一個(gè)關(guān)于所發(fā)生事情的消息)。
而如果使用 test_basic.py , test_expat.py 工具讓您可以分析 gnosis.xml.objectify 如何去描述一個(gè)新奇的 XML 文檔。但是與以前一樣,有很多我們可能想去驗(yàn)證的具體行為。 test_expat.py 的一個(gè)增強(qiáng)的、擴(kuò)展的版本使用 unittest 來分析各種動(dòng)作執(zhí)行時(shí)發(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 方式增添了相當(dāng)多的能力。我們可以將我們的測試分為幾個(gè)類,每一個(gè)類都繼承自 unittest.TestCase 。在每一個(gè)測試類內(nèi)部,每一個(gè)名稱以“.test”開始的方法都被認(rèn)為是另一個(gè)測試。為 ExpatTest 定義的兩個(gè)額外的類很有趣:在每次使用類執(zhí)行測試前運(yùn)行 .setUp() ,測試結(jié)束時(shí)運(yùn)行 .tearDown() (不管測試是成功、失敗還是出現(xiàn)錯(cuò)誤)。在我們上面的例子中,我們?yōu)閷S玫?expat_kwargs 字典做了一點(diǎn)簿記以確保每個(gè)測試獨(dú)立地運(yùn)行。
順便提一下,失敗(failure)和錯(cuò)誤(error)之間的區(qū)別很重要。一個(gè)測試可能會(huì)因?yàn)橐恍┚唧w的斷言無效而失?。〝嘌苑椒ɑ蛘咭浴?fail”開頭,或者以“.assert”開頭)。在某種意義上,失敗是期望中的 -- 最起碼從某種意義上我們已經(jīng)具體分析過。另一方面,錯(cuò)誤是意外的問題 -- 因?yàn)槲覀兪孪炔恢滥睦飼?huì)出錯(cuò),我們需要分析實(shí)際測試運(yùn)行中的回溯來診斷這種問題。不過,我們可以設(shè)計(jì)讓失敗給出診斷錯(cuò)誤的提示。例如,如果 Prerequisite.haveFiles() 失敗,將在一些 TestExpat 測試中出現(xiàn)錯(cuò)誤;如果前者是成功的,您將不得不到其他地方去查找錯(cuò)誤的根源。
在 unittest.TestCase 的繼承類中,具體的測試方法中可能會(huì)包括一些 .assert...() 或者 .fail...() 方法,但也可能只是具有一系列我們相信應(yīng)該會(huì)成功執(zhí)行的動(dòng)作。如果測試方法沒有按預(yù)期運(yùn)行,我們將得到一個(gè)錯(cuò)誤(以及描述這個(gè)錯(cuò)誤的回溯)。
test_expat.py 中的 _main_ 程序塊也值得察看。在最簡單的情況下,我們可以只使用 unittest.main() 來運(yùn)行測試用例,這將斷定哪些需要運(yùn)行。使用這種方式時(shí), unittest 模塊將接受一個(gè) -v 選項(xiàng)以給出更詳細(xì)的輸出。根據(jù)指定的文件名,在執(zhí)行了名稱空間設(shè)置后,我們打印出指定的 XML 文件的表示,從而大致上保持了對此工具稍老版本的向后兼容。
_main_ 中最有趣的分支是期待 -q 或 --quiet 標(biāo)簽的那個(gè)分支。如您將期望的,除非發(fā)生失敗或錯(cuò)誤,否則這個(gè)分支將是靜默的(quiet,即盡量減少輸出)。不僅如此,由于它是靜默的,它只會(huì)為每個(gè)問題顯示一行關(guān)于失敗/錯(cuò)誤位置的報(bào)告,而不是整個(gè)診斷回溯。除了對靜默輸出風(fēng)格的直接利用以外,這個(gè)分支還舉例說明了相對于測試套件的自定義測試以及對結(jié)果報(bào)告的控制。稍微有些長的 unittest.TextTestRunner() 的默認(rèn)輸出被定向到 StringIO out -- 如果您想查看它,歡迎您到 out.getvalue() 去查找。不過, result 對象讓我們對全面成功進(jìn)行測試,如果不是完全成功還可以讓我們處理失敗和錯(cuò)誤。顯然,由于它們是變量中的值,您可以輕松地將 result 對象的內(nèi)容記錄入日志,或者在 GUI 中顯示,不管怎么樣,不是僅僅打印到 STDOUT。
組合測試
可能 unittest 框架最好的特性是讓您可以輕松地組合包含不同模塊的測試。實(shí)際上,如果使用 Python 2.3+,您甚至可以將 doctest 測試轉(zhuǎn)化為 unittest 套件。讓我們將到目前為止所創(chuàng)建的測試組合到一個(gè)腳本 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í)行文檔字符串測試的轉(zhuǎn)換。讓我們來看看 test_all.py 運(yùn)行時(shí)會(huì)做什么:
清單 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 測試方法的情況下,他們的描述來自于相應(yīng)的 docstring 函數(shù)。如果您沒有指定文檔字符串,類和方法名被用作最合適的描述。來看一下如果一些測試失敗時(shí)我們會(huì)得到什么,同樣有趣(為本文去掉了回溯細(xì)節(jié)):
清單 7. 當(dāng)一些測試失敗時(shí)的結(jié)果
$ 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
隨便提及,這個(gè)失敗寫到 STDERR 的最后一行是“FAILED (failures=1, errors=2)”,如果您需要的話這是一個(gè)很好的總結(jié)(相對于成功時(shí)最終的“OK”)。
從這里開始
本文向您介紹了 unittest 和 doctest 的一些典型用法,它們已經(jīng)改進(jìn)了我自己的軟件中的測試。閱讀 Python 文檔,以深入了解可用于測試套件、測試用例和測試結(jié)果的全部范圍的方法。它們?nèi)慷甲裱又兴枋龅哪J健?/p>
讓自己遵從 Python 的標(biāo)準(zhǔn)測試模塊規(guī)定的方法學(xué)是良好的軟件實(shí)踐。測試驅(qū)動(dòng)(test-driven)的開發(fā)在很多軟件周期中都很流行;不過,顯然 Python 是一門適合于測試驅(qū)動(dòng)模型的語言。而且,如果只是考慮軟件包更可能按計(jì)劃工作,一個(gè)軟件包或庫如果伴隨有一組周全的測試,會(huì)比缺乏這些測試的軟件包或庫對用戶更為有用。
相關(guān)文章
Python實(shí)現(xiàn)的從右到左字符串替換方法示例
這篇文章主要介紹了Python實(shí)現(xiàn)的從右到左字符串替換方法,涉及Python字符串遍歷、運(yùn)算、判斷、替換等相關(guān)操作技巧,需要的朋友可以參考下2018-07-07python開發(fā)之tkinter實(shí)現(xiàn)圖形隨鼠標(biāo)移動(dòng)的方法
這篇文章主要介紹了python開發(fā)之tkinter實(shí)現(xiàn)圖形隨鼠標(biāo)移動(dòng)的方法,涉及Python基于tkinter繪圖的相關(guān)實(shí)現(xiàn)技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-11-11Flask框架的學(xué)習(xí)指南之制作簡單blog系統(tǒng)
本文是Flask框架的學(xué)習(xí)指南系列文章的第二篇主要給大家講述制作一個(gè)簡單的小項(xiàng)目blog系統(tǒng)的過程,有需要的小伙伴可以參考下2016-11-11Python實(shí)現(xiàn)的簡單模板引擎功能示例
這篇文章主要介紹了Python實(shí)現(xiàn)的簡單模板引擎功能,結(jié)合具體實(shí)例形式分析了Python模版引擎的定義與使用方法,需要的朋友可以參考下2017-09-09Python使用Scrapy保存控制臺(tái)信息到文本解析
這篇文章主要介紹了Python使用Scrapy保存控制臺(tái)信息到文本解析,具有一定借鑒價(jià)值,需要的朋友可以參考下2017-12-12Python時(shí)間管理黑科技之datetime函數(shù)詳解
在Python中,datetime模塊是處理日期和時(shí)間的標(biāo)準(zhǔn)庫,它提供了一系列功能強(qiáng)大的函數(shù)和類,用于處理日期、時(shí)間、時(shí)間間隔等,本文將深入探討datetime模塊的使用方法,感興趣的可以了解下2023-08-08