軟體工程/測試/測試驅動開發簡介
測試驅動開發 (TDD) 是一種軟體開發流程,它依賴於一個非常短的開發週期的重複:首先開發人員編寫一個失敗的自動化測試用例,該用例定義了所需的改進或新功能,然後生成程式碼以透過該測試,最後將新程式碼重構到可接受的標準。肯特·貝克 (Kent Beck) 因開發或“重新發現”該技術而受到讚譽,他在 2003 年表示,TDD 鼓勵簡單的設計並增強信心。[1]
測試驅動開發與 1999 年開始的極限程式設計的測試優先程式設計概念相關。[2] 但最近它本身也引起了更廣泛的興趣。[3]
程式設計師還將此概念應用於改進和除錯使用舊技術開發的遺留程式碼。[4]
測試驅動開發要求開發人員在編寫程式碼本身之前(立即)建立定義程式碼需求的自動化單元測試。測試包含真或假的斷言。當開發人員改進和重構程式碼時,透過測試確認正確的行為。開發人員通常使用測試框架(例如 xUnit)來建立和自動執行一組測試用例。
以下序列基於書籍《測試驅動開發的實踐》。[1]
在測試驅動開發中,每個新功能都從編寫測試開始。此測試必然會失敗,因為它是在實現該功能之前編寫的。(如果它沒有失敗,那麼要麼提議的“新”功能已經存在,要麼測試有缺陷。)為了編寫測試,開發人員必須清楚地瞭解該功能的規範和需求。開發人員可以透過使用用例和使用者故事來完成此操作,這些用例和使用者故事涵蓋了需求和異常情況。這也可能意味著現有測試的變體或修改。這是測試驅動開發與在編寫程式碼之後編寫單元測試的區別特徵:它使開發人員在編寫程式碼之前專注於需求,這是一個細微但重要的區別。
這驗證了測試工具是否正常工作,並且新測試不會在不需要任何新程式碼的情況下錯誤地透過。此步驟還會對測試本身進行負面測試:它排除了新測試始終透過的可能性,因此毫無價值。新測試還應因預期原因而失敗。這增加了信心(儘管它並不能完全保證)它正在測試正確的事物,並且僅在預期的情況下才會透過。
下一步是編寫一些程式碼,使測試透過。在此階段編寫的程式碼不會是完美的,例如,它可能以一種不優雅的方式透過測試。這是可以接受的,因為後續步驟將對其進行改進和最佳化。
重要的是,編寫的程式碼僅用於透過測試;在任何階段都不應預測和“允許”任何進一步的(因此未經測試的)功能。
如果所有測試用例現在都透過,則程式設計師可以確信程式碼滿足所有經過測試的需求。這是一個開始週期最後一步的好起點。
現在可以根據需要清理程式碼。透過重新執行測試用例,開發人員可以確信程式碼重構不會損壞任何現有功能。消除重複的概念是任何軟體設計的重點。但是,在這種情況下,它也適用於消除測試程式碼和生產程式碼之間的任何重複 - 例如在兩者中重複的幻數或字串,以便在步驟 3 中使測試透過。
從另一個新的測試開始,然後重複該迴圈以推動功能向前發展。步驟的大小應始終很小,每次測試執行之間只有 1 到 10 次編輯。如果新程式碼沒有快速滿足新測試,或者其他測試意外失敗,則程式設計師應優先選擇撤消或回退,而不是進行過度除錯。持續整合透過提供可回退的檢查點來提供幫助。在使用外部庫時,重要的是不要進行過小的增量,以至於實際上僅僅是在測試庫本身,[3] 除非有理由相信庫存在錯誤或功能不完整,無法滿足正在編寫的程式的所有需求。
測試驅動開發包含多個方面,例如“保持簡單,傻瓜”(KISS)和“你不需要它”(YAGNI)等原則。透過專注於編寫僅透過測試所需的程式碼,設計可以比其他方法更簡潔明瞭。[1] 在肯特·貝克的《測試驅動開發實踐》一書中,他還提出了“先假裝,後實現”的原則。
為了實現某些高階設計理念(例如設計模式),會編寫生成該設計的測試。程式碼可能仍然比目標模式簡單,但仍然可以透過所有必需的測試。這最初可能會讓人感到不安,但它允許開發人員只關注重要的內容。
**先寫測試**。測試應該在被測試的功能之前編寫。據說這樣做有兩個好處。它有助於確保應用程式的可測試性,因為開發人員必須從一開始就考慮如何測試應用程式,而不是以後再擔心。它還確保為每個功能編寫測試。當編寫功能優先的程式碼時,開發人員和開發組織往往會將開發人員推向下一個功能,完全忽略測試。
**先讓測試用例失敗**。這樣做的目的是確保測試確實有效,並且可以捕獲錯誤。一旦證明這一點,就可以實現底層功能。這被稱為“測試驅動開發箴言”,也稱為紅/綠/重構,其中紅色表示*失敗*,綠色表示*透過*。
測試驅動開發不斷重複新增失敗的測試用例、使其透過以及重構的步驟。在每個階段獲得預期的測試結果會強化程式設計師對程式碼的心理模型,增強信心並提高生產力。
測試驅動開發的高階實踐可以導致驗收測試驅動開發 (ATDD),其中客戶指定的標準被自動化為驗收測試,然後驅動傳統的單元測試驅動開發 (UTDD) 過程。[5] 這個過程確保客戶擁有一個自動化的機制來決定軟體是否滿足他們的需求。透過 ATDD,開發團隊現在有一個特定的目標需要滿足,即驗收測試,這使他們持續專注於客戶真正想要從使用者故事中獲得什麼。
2005 年的一項研究發現,使用 TDD 意味著編寫更多測試,而反過來,編寫更多測試的程式設計師往往生產力更高。[6] 與程式碼質量和 TDD 與生產力之間更直接的相關性相關的假設尚無定論。[7]
在新的(“綠地”)專案上使用純 TDD 的程式設計師報告說,他們很少需要呼叫偵錯程式。與版本控制系統結合使用時,當測試意外失敗時,將程式碼恢復到透過所有測試的最後一個版本通常比除錯更有成效。[8]
測試驅動開發提供的不僅僅是簡單的正確性驗證,還可以驅動程式的設計。透過首先關注測試用例,必須想象客戶端將如何使用該功能(在第一種情況下,是測試用例)。因此,程式設計師在實現之前關注的是介面。這種優勢與契約式設計相輔相成,因為它透過測試用例而不是數學斷言或先入為主的概念來處理程式碼。
測試驅動開發提供了在需要時採取小步驟的能力。它允許程式設計師專注於手頭的任務,因為第一個目標是使測試透過。異常情況和錯誤處理最初不會被考慮,並且建立這些額外情況的測試會單獨實現。測試驅動開發以這種方式確保所有編寫的程式碼都至少由一個測試覆蓋。這使程式設計團隊和後續使用者對程式碼更有信心。
雖然 TDD 比不使用 TDD 需要更多程式碼(因為有單元測試程式碼),但總的程式碼實現時間通常更短。[9] 大量測試有助於限制程式碼中的缺陷數量。測試的早期和頻繁性質有助於在開發週期的早期捕獲缺陷,防止它們成為流行且代價高昂的問題。在流程早期消除缺陷通常可以避免在專案後期進行冗長而乏味的除錯。
TDD 可以導致更模組化、更靈活和更可擴充套件的程式碼。這種效果通常是因為該方法要求開發人員以可以獨立編寫和測試並稍後整合在一起的小單元的形式來考慮軟體。這導致更小、更集中的類、更鬆散的耦合和更簡潔的介面。模擬物件設計模式的使用也有助於程式碼的整體模組化,因為這種模式要求編寫程式碼,以便可以在單元測試的模擬版本和部署的“真實”版本之間輕鬆切換模組。
因為不會編寫超過透過失敗測試用例所需的程式碼,所以自動化測試傾向於覆蓋每個程式碼路徑。例如,為了讓 TDD 開發人員向現有的 if 語句新增 else 分支,開發人員首先必須編寫一個導致該分支失敗的測試用例。因此,來自 TDD 的自動化測試往往非常徹底:它們會檢測程式碼行為中的任何意外更改。這可以檢測到在開發週期的後期更改意外更改其他功能時可能出現的問題。
- 在需要完整的功能測試來確定成功或失敗的情況下,測試驅動開發很難使用。這些示例包括使用者介面、與資料庫一起工作的程式以及某些依賴於特定網路配置的程式。TDD 鼓勵開發人員將最少的程式碼放入此類模組中,並最大化可測試庫程式碼中的邏輯,使用模擬和模擬來表示外部世界。
- 管理支援至關重要。如果沒有整個組織相信測試驅動開發將改進產品,管理層可能會認為花費在編寫測試上的時間是被浪費的。[10]
- 在測試驅動開發環境中建立的單元測試通常由也將編寫被測試程式碼的開發人員建立。因此,測試可能與程式碼共享相同的盲點:例如,如果開發人員沒有意識到必須檢查某些輸入引數,那麼測試和程式碼很可能都不會驗證這些輸入引數。如果開發人員誤解了正在開發的模組的需求規範,那麼測試和程式碼都將是錯誤的。
- 大量的透過單元測試可能會帶來一種虛假的安全感,導致更少的其他軟體測試活動,例如整合測試和合規性測試。
- 測試本身成為專案維護開銷的一部分。編寫不當的測試,例如包含硬編碼錯誤字串或本身容易出錯的測試,維護成本很高。存在定期產生錯誤失敗的測試會被忽略的風險,因此當出現真實失敗時,它可能不會被檢測到。可以編寫易於維護的測試,例如透過重用錯誤字串,這應該是在上面描述的程式碼重構階段的目標。
- 在重複的 TDD 週期中實現的覆蓋率和測試細節水平無法輕鬆地在以後重新建立。因此,隨著時間的推移,這些原始測試變得越來越寶貴。如果糟糕的架構、糟糕的設計或糟糕的測試策略導致後期更改導致數十個現有測試失敗,則必須單獨修復它們。僅僅刪除、停用或草率地更改它們會導致測試覆蓋率中無法檢測到的漏洞。
測試套件程式碼顯然必須能夠訪問它正在測試的程式碼。另一方面,不應損害資訊隱藏、封裝和關注點分離等正常的設計標準。因此,TDD 的單元測試程式碼通常與被測試的程式碼寫在同一個專案或模組中。
在面向物件的設計中,這仍然無法訪問 private 資料和方法。因此,單元測試可能需要額外的工作。在 Java 和其他語言中,開發人員可以使用反射來訪問標記為 private 的欄位。[11] 或者,可以使用內部類來儲存單元測試,以便它們可以檢視封閉類的成員和屬性。在 .NET Framework 和一些其他程式語言中,可以使用部分類來公開供測試訪問的私有方法和資料。
重要的是,此類測試技巧不應該保留在生產程式碼中。在 C 和其他語言中,編譯器指令(例如`#if DEBUG ... #endif`)可以放置在這些附加類以及所有其他與測試相關的程式碼周圍,以防止它們被編譯到釋出的程式碼中。這意味著釋出的程式碼與單元測試的程式碼不完全相同。然後,定期執行數量更少但更全面、端到端的整合測試,可以確保(除其他事項外)不存在任何以測試框架的某些方面為基礎的生產程式碼。
關於是否明智地測試私有和受保護的方法和資料,TDD 實踐者在其部落格和其他著作中存在一些爭論。一些人認為,透過其公共介面測試任何類就足夠了,因為私有成員僅僅是實現細節,可能會發生變化,並且應該允許這樣做,而不會破壞大量的測試。另一些人則認為,關鍵的功能方面可能在私有方法中實現,並且僅透過公共介面間接地開發和測試它會模糊問題:單元測試是關於測試儘可能小的功能單元。[12][13]
單元測試之所以這樣命名,是因為它們每個都測試程式碼的一個單元。一個複雜的模組可能有 1000 個單元測試,而一個簡單的模組可能只有 10 個。用於 TDD 的測試永遠不應該跨越程式中的程序邊界,更不用說網路連線了。這樣做會引入延遲,導致測試執行緩慢,並阻止開發人員執行整個套件。引入對外部模組或資料的依賴也會將單元測試變成整合測試。如果一個模組在一系列相互關聯的模組中行為異常,則不清楚在哪裡查詢故障原因。
當正在開發的程式碼依賴於資料庫、Web 服務或任何其他外部程序或服務時,強制執行可單元測試的分離也是一個機會,也是設計更模組化、更可測試和更可重用程式碼的驅動力。[14] 需要兩個步驟
- 無論何時最終設計中需要外部訪問,都應該定義一個介面來描述可用的訪問許可權。有關執行此操作的好處(無論是否使用 TDD),請參閱依賴倒置原則。
- 該介面應以兩種方式實現,一種真正訪問外部程序,另一種是模擬物件或樁物件。模擬物件只需要將訊息(例如“Person 物件已儲存”)新增到跟蹤日誌中,測試斷言可以針對該日誌執行以驗證正確行為。樁物件的不同之處在於,它們本身包含測試斷言,這些斷言可以使測試失敗,例如,如果人員的姓名和其他資料與預期不符。返回資料的模擬物件和樁物件方法(表面上來自資料儲存或使用者)可以透過始終返回測試可以依賴的相同、真實的資料來幫助測試過程。它們還可以設定為預定義的故障模式,以便可以開發和可靠地測試錯誤處理例程。除資料儲存之外的其他模擬服務在 TDD 中也可能很有用:模擬加密服務實際上可能不會加密傳遞的資料;模擬隨機數服務可能始終返回 1。模擬或樁實現是依賴注入的示例。
這種依賴注入的推論是,TDD 過程本身永遠不會測試實際的資料庫或其他外部訪問程式碼。為了避免由此產生的錯誤,需要其他測試,這些測試使用上述介面的“真實”實現例項化測試驅動的程式碼。這些測試與 TDD 單元測試完全分開,實際上是整合測試。它們的數量會更少,並且需要比單元測試執行得更少。儘管如此,它們可以使用相同的測試框架(例如 xUnit)來實現。
更改任何永續性儲存或資料庫的整合測試應始終謹慎設計,並考慮檔案或資料庫的初始狀態和最終狀態,即使任何測試失敗也是如此。這通常透過以下技術的某種組合來實現
- `TearDown` 方法,它是許多測試框架不可或缺的一部分。
- 在可用時使用`try...catch...finally` 異常處理結構。
- 資料庫事務,其中事務以原子方式包含寫入、讀取和匹配刪除操作。
- 在執行任何測試之前獲取資料庫的“快照”,並在每次測試執行後回滾到該快照。這可以使用 Ant 或 NAnt 等框架或 CruiseControl 等持續整合系統來自動化。
- 在測試之前將資料庫初始化為乾淨狀態,而不是在測試之後清理。這可能與清理可能會使診斷測試失敗變得困難相關,因為在執行詳細診斷之前刪除了資料庫的最終狀態。
Moq、jMock、NMock、EasyMock、Typemock、jMockit、Unitils、Mockito、Mockachino、PowerMock 或 Rhino Mocks 等框架的存在使得建立和使用複雜的模擬物件的過程變得更容易。
- ↑ a b c Beck, K. 示例驅動的測試開發,Addison Wesley,2003
- ↑ Lee Copeland(2001 年 12 月)。"極限程式設計"。Computerworld. 檢索於 2011 年 1 月 11 日.
- ↑ a b Newkirk,JW 和 Vorontsov,AA。Microsoft .NET 中的測試驅動開發,Microsoft Press,2004 年。
- ↑ Feathers,M。有效使用遺留程式碼,Prentice Hall,2004 年
- ↑ Koskela,L。“測試驅動:Java 開發人員的 TDD 和驗收 TDD”,Manning 出版物,2007 年
- ↑ Erdogmus,Hakan。“關於測試優先程式設計方法的有效性”。IEEE 軟體工程學報論文集,31(1)。2005 年 1 月。(NRC 47445). 檢索於 2008 年 1 月 14 日。
我們發現,測試優先的學生平均編寫了更多測試,而編寫更多測試的學生往往生產力更高。
{{cite web}}: 未知引數|coauthors=被忽略(建議使用|author=)(幫助) - ↑ Proffitt,Jacob。“TDD 證明有效!還是?”. 檢索於 2008 年 2 月 21 日。
因此,TDD 與質量的關係充其量是有問題的。它與生產力的關係更有趣。我希望有後續研究,因為生產力資料對我來說根本加不起來。生產力和測試數量之間存在不可否認的相關性,但這種相關性在非 TDD 組中實際上更強(該組有一個異常值,而 TDD 組大約有一半在 95% 範圍內之外)。
- ↑ Llopis, Noel (2005年2月20日). "透過鏡子的世界:測試驅動遊戲開發(第一部分)". Games from Within. 檢索日期 2007-11-01.
將[TDD]與非測試驅動開發方法進行比較,您將用程式碼替換所有心理檢查和偵錯程式步驟,以驗證您的程式是否完全按照您的預期執行。
- ↑ Müller, Matthias M. "關於測試驅動開發的投資回報率" (PDF). 卡爾斯魯厄大學,德國. 第 6頁. 檢索日期 2007-11-01.
{{cite web}}: 未知引數|coauthors=被忽略(建議使用|author=)(幫助) - ↑ Loughran, Steve (2006年11月6日). "測試" (PDF). HP實驗室. 檢索日期 2009-08-12.
- ↑ Burton, Ross (2003年11月12日). "顛覆Java訪問保護以進行單元測試". O'Reilly Media, Inc. 檢索日期 2009-08-12.
{{cite web}}: 檢查日期值:|date=(幫助) - ↑ Newkirk, James (2004年6月7日). "測試私有方法/成員變數 - 您應該還是不應該". 微軟公司. 檢索日期 2009-08-12.
- ↑ Stall, Tim (2005年3月1日). "如何在.NET中測試私有和受保護的方法". CodeProject. 檢索日期 2009-08-12.
- ↑ Fowler, Martin (1999). 重構 - 改善既有程式碼的設計. 波士頓:Addison Wesley Longman, Inc. ISBN 0-201-48567-2.
- [1] 在WikiWikiWeb上
- 測試還是規範?測試和規範?從規範中測試!,作者:Bertrand Meyer(2004年9月)
- 從TDD方法進行Microsoft Visual Studio Team Test
- 編寫可維護的單元測試,為您節省時間和淚水
- 使用測試驅動開發(TDD)提高應用程式質量