關于測試的若干誤解
作者 Liam O'Connor 譯者 高翌翔
本文中所表達的觀點僅代表Liam O'Connor個人意見,與其雇主(NICTA)無關。
如果說你我之間有什么相似之處的話,那就是你可能閱讀過大量文章,在其中作者主張測試驅動開發(TDD,Test-Driven Development)或者其他涵蓋了廣泛測試(無論是單元測試還是集成測試層面上)的開發實踐。我認為,關于這些實踐的許多主張缺乏實際項目經驗,很難讓人相信他們的觀點。事實上,當我們把這些非常嚴格的測試實踐應用于大型項目上時,通常它們根本無法順利工作。
在本文中,我將說明一些關于測試的常見誤解。我希望,如果你在編寫測試時也存在這樣的誤解,那么本文能幫助你和你的團隊來判斷何時適合測試,何時不適合測試。
誤解一:測試可以表明我的代碼是正確的!
雖然這種誤解在直覺上是正確的,但是你確實無法依賴測試來建立任何形式的具有嚴格正確性的標準。每當你編寫了一個測試,你就已經測試了程序中的一種可能情況。當程序中存在許多單元時,或許存在無限多種(或是多得難以應付的)可能的情況需要測試時,那么測試所有可能情況是不可行的。因此,典型的對策是測試一些出錯情況、邊界情況以及若干恰好確保一切正常的常規情況。
如果你的目標是正確性,那么上面談到內容還遠不足以滿足要求。盡管程序仍存在許多bug,但是開發一套總是可以通過的測試還是相當容易的。然而有些bug根本不可能通過測試檢查出來,其中競爭條件和包括并發性在內的其他錯誤都是經典的例證,即使你已經對調度程序進行控制,然而可能的交錯操作的數量增長是如此之快,以至于可靠地測試很快成為了不可能完成的任務。
因此,測試無法展示所有情況下的正確性,除非是在最普通的情況下,那樣我們可以在測試中完全指定程序單元的行為。對于這些普通情況,往往不值得從一開始就編寫測試;之所以說這些情況實在是太普通了,是因為我們所要測試的代碼本身就是微不足道的!通過為那些微不足道的代碼片段編寫測試只完成了一件事,那就是增加維護開銷,并且為測試機增加工作量。
既然測試也只是一些代碼,那么在你的測試中同樣可能存在bug。如果編寫測試與編寫代碼的是同一個人,那么他們往往可能錯誤地實現一個程序單元,然后編寫一個確保那個錯誤行為能夠通過的測試。此問題的根源在于開發者誤解了規格說明,而不是實現過程中犯下的小錯誤。
如果你確實需要保證正確性,那么請對你的代碼進行形式化驗證(目前的驗證工具要比過去好得多)。如果你不需要保證正確性,那么編寫測試就可以了。須牢記,編寫測試的作用就如同煙霧報警器對于火災的作用一樣,其實它并不能檢測出各種各樣的問題。
誤解二:測試是可執行的規格說明!
基于以下幾個原因我認為這個觀點是錯誤的。先來看看在我的字典里規格說明的定義:
一組需求,用于界定對于某一對象或過程的準確描述。
因此,如果我的代碼符合規格說明的要求,那么它就應該是完全正確的,因為規格說明準確界定了代碼的行為。如果說我的測試是規格說明,那么必須進而證明測試的正確性。正如我們已經討論過的,測試并沒有做這樣的事情,因此測試不是規格說明。
讓我們看下實際情況,假設一名開發者通過閱讀測試用例可以推斷出某個函數的預期行為,然后引入一大堆含混不清的測試用例;如果測試用例不夠全面,那么我們可能最終推斷出錯誤的結論,有時可能與預期行為僅有細微差別。
此外,對于測試用例并未進行一致性檢查。也就是說,由于開發者失誤或誤解,因此你的測試可能實際指定了一個非預期的行為。這可能會導致在你的測試中出現一些矛盾,因此也可以說你的規格說明中出現了矛盾。
隨機測試軟件,例如QuickCheck,會讓編寫測試的工作變得非常簡單,就像本應包含的布爾屬性一樣,而且該軟件會為你生成測試用例。該軟件使得測試更接近于可執行的規格說明,不過它仍然不會對屬性進行一致性檢查。
誤解三:測試會讓我們擁有良好的設計!
當讓一個糟糕的設計可以測試時,此設計仍然具有改進的可能,因此測試不是優良設計實踐的替代品。當為系統接口編寫大量的測試時,實際上是增加了開發者投入在那些接口上的工作投資。當這些接口不再是最佳選擇時問題就會隨之產生,即開發者已經為那些接口編寫了大量測試。改變接口也就意味著改變所有與之配套的測試。由于測試與那些接口緊密耦合在一起,因此其中大多數測試將必須被廢棄并重寫。既然大多數開發者的成長依附于他們所從事的工作,這會導致在項目的生命周期中對于那些次優的設計決策躊躇不前,盡管那些決策不是最適合的。
在這里給出的解決方案是,只有在你編寫了一系列原型之后再開始著手測試。這樣你就不必為測試那些可能在稍后會被大量重構的代碼而焦慮。對于開發者和測試機而言,所做的一切都是在增加工作量,而且當需求或接口改變時,開發者必須銷毀數小時的工作成果,這會使他們更心痛。而如果你不等待而進行了測試,那么你的測試實際上會導致糟糕的設計,因為開發者將不愿進行任何重大的重構。
此外,讓代碼可以測試很困難。通常人們僅僅為了讓測試更加容易而采用有問題的設計決策;嘗試大量模擬接口實現,或者是編寫具有大量代碼的測試用例,以至于測試用例代碼本身幾乎也需要測試,這些做法都暴露出對于抽象的泄漏(mock對象和stub往往會經受此問題的折磨)。
誤解四:測試會讓更改代碼更容易!
測試并不總是讓更改代碼更容易,然而,如果你正在對底層接口實現進行修改,那么測試可以幫助你捕獲新實現中的功能衰退或非預期的行為。如果你正在對程序的更高層次結構進行修改,然而這種對立的情況則是更普遍的現象。測試通常與更高層次接口緊密耦合。改變這些接口就意味著重寫測試。在那種情況下,你讓自己活得很辛苦你將必須重寫那些測試,從而給自己增加了更多的工作,而且之前的舊測試對于確保你沒有引入功能衰退而言無能為力,這意味著測試根本幫不上忙。
所以,不寫測試?
我沒有說你不應該編寫測試。對于提高信心以及阻止軟件功能衰退而言,測試是一種有價值的方式。然而,測試無法統一帶來優良的設計、正確性、技術規格說明或者輕松地重構,至于原因如上所述。過度使用測試會讓開發變得*更難*而不是更容易。
同樣,根本不驗證代碼會讓質量保證無從談起,不過會讓快速構建原型更輕松。測試在質量保證與靈活性之間引入了一個權衡問題,所以我們必須在二者之間做出適當的妥協。
關于作者
Liam O'Connor曾任職于Google,并任教于新南威爾士大學。最近,他開始為NICTA的l4.verified項目工作,此項目是對操作系統內核進行形式化驗證,NICTA是澳大利亞領先的ICT(Information and Communications Technology,信息與通訊技術)研究機構。
譯者評論
俗話說尺有所短寸有所長,此話與No silver bullet有異曲同工之妙。既然世間沒有包治百病的靈丹妙藥,那么就應對癥下藥,而且下藥前最好弄明白藥品的功效及禁忌,否則吃錯藥的后果不堪設想。
言歸正傳,測試的功效不可否定,但對其禁忌的說明卻沒有那么清晰。本文作者提出了幾點對于測試禁忌的看法,或許個別觀點有失偏頗,但是其目的是希望大家可以更加客觀、全面地認識TDD及測試。