【敏捷開發】驅動測試開發

Leo.cheng發表於2014-03-14

測試驅動開發介紹

  1. 測試驅動開發(Test Driven Development,英文縮寫TDD)是極限程式設計的一個重要組成部分
  2. 它的基本思想就是在開發功能程式碼之前,先編寫測試程式碼
  3. 也就是說在明確要開發某個功能後
    1. 首先思考如何對這個功能進行測試,並完成測試程式碼的編寫
    2. 然後編寫相關的程式碼滿足這些測試用例
    3. 然後迴圈進行新增其他功能,直到完成全部功能的開發。
  4. 程式碼整潔可用(clean code that works) 是測試驅動開發所追求的目標。
  5. 雖然TDD光大於極限程式設計,但測試驅動開發完全可以單獨應用。 

極限程式設計

  1. 極限程式設計誕生於一種加強開發者與使用者的溝通需求,讓客戶全面參與軟體的開發設計,保證變化的需求及時得到修正。
  2. 要讓客戶能方便地與開發人員溝通,一定要用客戶理解的語言
  3. 先測試再編碼就是先給客戶軟體的外部輪廓,客戶使用的功能展現,讓客戶感覺到未來軟體的樣子
  4. 先測試再編碼與瀑布模型顯然是背道而馳的。
  5. 同時,極限程式設計注重使用者反饋與讓客戶加入開發是一致的,讓客戶參與就是隨時反饋軟體是否符合客戶的要求。
  6. 有了反饋,開發子過程變短,迭代也就很自然出現了, 快速迭代,小版本釋出都讓開發過程變成更多的自反饋過程。
     

測試驅動開發的優點

    跟測試後進行的開發方式相比,它有如下好處:
  1. 測試驅動開發最重要的功能還在於保障程式碼的正確性,能夠迅速發現、定位bug。針對關鍵程式碼的測試集,以及不斷完善的測試用例,為迅速發現、定位bug提供了條件。
  2. 通過編寫程式碼的測試用例,對其功能的分解、使用過程、介面都進行了設計。而且這種從使用角度對程式碼的設計通常更符合後期開發的需求。可測試的要求,對程式碼的內聚性的提高和複用都非常有益。因此測試驅動開發也是一種程式碼設計的過程。
  3. 寫單元測試的時候,很容易就可以為一個行為寫一個測試用例,讓它通過,然後為另一種行為寫另一個測試用例。也就是說,整個任務會被劃分成很多小的任務,獨立完成。如果我們不用TDD而直接實現的話,我們很容易就會同時把所有的行為都實現了。這樣花的時間長,而且在這相當長的時間裡面,寫的程式碼都是沒有測試 過,不能保證準確性的。相反的,用TDD的話,我們只實現要測的行為的程式碼。它只花費很少的時間(幾分鐘),而且可以馬上測試。
  4. 為了更容易的寫單元測試,我們會廣泛的使用介面。這個會讓單元測試程式碼很容易讀跟寫, 因為測試程式碼裡面沒有多餘的資料。如果我們不用TDD而是直接寫實現的話,我們經常會使用現成的類,測試為了呼叫現成的類, 就不得不建立很多多餘的資料,建立很巨型的物件。
  5. 因為廣泛的使用介面,我們的類之間就不會藕合,因此重用性更好
  6. 發現比傳統測試方式更多的Bug。
  7. 完工時完工。表明我可以很清楚的看到自己的這段工作已經結束了,而傳統的方式很難知道什麼時候編碼工作結束了。

一個生動比喻

  1. 蓋房子的時候,工人師傅砌牆,會先用樁子拉上線,以使磚能夠壘的筆直,因為壘磚的時候都是以這根線為基準的。
  2. TDD就像這樣,先寫測試程式碼,就像工人師傅先用樁子拉上線,然後編碼的時候以此為基準,只編寫符合這個測試的功能程式碼。
  3. 而一個新手或菜鳥級的小師傅,卻可能不知道拉線,而是直接把磚往上壘,壘了一些之後再看是否筆直,這時候可能會用一根線,量一下砌好的牆是否筆直,如果不直再進行校正,敲敲打打。
  4. 使用傳統的軟體開發過程就像這樣,我們先編碼,編碼完成之後才寫測試程式,以此檢驗已寫的程式碼是否正確,如果有錯誤再一點點修改。

測試驅動開發的過程

  1. 明確當前要完成的功能。可以記錄成一個 TODO 列表。
  2. 快速完成針對此功能的測試用例編寫。
  3. 測試程式碼編譯不通過。
  4. 編寫對應的功能程式碼。
  5. 測試通過。
  6. 對程式碼進行重構,並保證測試通過。
  7. 迴圈完成所有功能的開發。 

測試驅動開發的原則

  1. 測試隔離。不同程式碼的測試應該相互隔離。對一塊程式碼的測試只考慮此程式碼的測試,不要考慮其實現細節(比如它使用了其他類的邊界條件)。
  2. 一頂帽子。開發人員開發過程中要做不同的工作,比如:編寫測試程式碼、開發功能程式碼、對程式碼重構等。做不同的事,承擔不同的角色。開發人員完成對應的工作時應該保持注意力集中在當前工作上,而不要過多的考慮其他方面的細節,保證頭上只有一頂帽子。避免考慮無關細節過多,無謂地增加複雜度。
  3. 測試列表。需要測試的功能點很多。應該在任何階段想新增功能需求問題時,把相關功能點加到測試列表中,然後繼續手頭工作。然後不斷的完成對應的測試用例、功能程式碼、重構。一是避免疏漏,也避免干擾當前進行的工作。
  4. 測試驅動。這個比較核心。完成某個功能,某個類,首先編寫測試程式碼,考慮其如何使用、如何測試。然後在對其進行設計、編碼。
  5. 先寫斷言。測試程式碼編寫時,應該首先編寫對功能程式碼的判斷用的斷言語句,然後編寫相應的輔助語句。
  6. 可測試性。功能程式碼設計、開發時應該具有較強的可測試性。其實遵循比較好的設計原則的程式碼都具備較好的測試性。比如比較高的內聚性,儘量依賴於介面等。
  7. 及時重構。無論是功能程式碼還是測試程式碼,對結構不合理,重複的程式碼等情況,在測試通過後,及時進行重構。
  8. 小步前進。軟體開發是個複雜性非常高的工作,開發過程中要考慮很多東西,包括程式碼的正確性、可擴充套件性、效能等等,很多問題都是因為複雜性太大導致的。極限程式設計提出了一個非常好的思路就是小步前進。把所有的規模大、複雜性高的工作,分解成小的任務來完成。對於一個類來說,一個功能一個功能的完成,如果太困難就再分解。每個功能的完成就走測試程式碼-功能程式碼-測試-重構的迴圈。通過分解降低整個系統開發的複雜性。這樣的效果非常明顯。幾個小的功能程式碼完成後,大的功能程式碼幾乎是不用除錯就可以通過。一個個類方法的實現,很快就看到整個類很快就完成。本來感覺很多特性需要增加,很快就會看到 沒有幾個啦。你甚至會為這個速度感到震驚。(個人理解,是大幅度減少除錯、出錯的時間產生的這種速度感)

測試範圍、粒度

  1. 對哪些功能進行測試?會不會太繁瑣?什麼時候可以停止測試?這些問題比較常見。按大師 Kent Benk 的話,對那些你認為應該測試的程式碼進行測試。就是說,要相信自己的感覺,自己的經驗。那些重要的功能、核心的程式碼就應該重點測試。感到疲勞就應該停下來休息一下。感覺沒有必要更詳細的測試,就停止本輪測試。
  2. 測試驅動開發強調測試並不應該是負擔,而應該是幫助我們減輕工作量的方法。而對於何時停止編寫測試用例,也是應該根據你的經驗,功能複雜、核心功能的程式碼就應該編寫更全面、細緻的測試用例,否則測試流程即可。
  3. 測試範圍沒有靜態的標準,同時也應該可以隨著時間改變。對於開始沒有編寫足夠的測試的功能程式碼,隨著bug的出現,根據bug補齊相關的測試用例即可。
  4. 小步前進的原則,要求我們對大的功能塊測試時,應該先分拆成更小的功能塊進行測試,比如一個類A使用了類B、C,就應該編寫到A使用B、C功能的測試程式碼前,完成對B、C的測試和開發。那麼是不是每個小類或者小函式都應該測試哪?個人認為沒有必要。應該運用你的經驗,對那些可能出問題的地方重點測試,感覺不可能出問題的地方就等它真正出問題的時候再補測試。

怎麼編寫測試用例

  1. 測試用例的編寫就用上了傳統的測試技術。
  2. 操作過程儘量模擬正常使用的過程。
  3. 全面的測試用例應該儘量做到分支覆蓋,核心程式碼儘量做到路徑覆蓋。
  4. 測試資料儘量包括:真實資料、邊界資料。
  5. 測試語句和測試資料應該儘量簡單,容易理解。
  6. 為了避免對其他程式碼過多的依賴,可以實現簡單的樁函式或樁類(Mock Object)。
  7. 如果內部狀態非常複雜或者應該判斷流程而不是狀態,可以通過記錄日誌字串的方式進行驗證。

單元測試

概述

  1. 單元測試的目標:確保模組被正確地編碼
  2. 由誰去做:通常由開發人員執行。
  3. 怎樣去測試:功能測試可以用黑盒測試方法,程式碼測試可用白盒測試方法。
  4. 什麼時候停止:當開發人員感到程式碼沒有缺陷時。

單元測試的重要性

  1. 一個盡責的單元測試方法將會在產品開發的某個階段發現很多的Bug,並且修改它們的成本也很低。
  2. 系統開發的後期階段,Bug的檢測和修改將會變得更加困難,並要消耗大量的時間和開發費用。
  3. 無論什麼時候做出修改都要進行完整的迴歸測試,在生命週期中儘早的對產品程式碼進行測試將是效率和質量得到最好的保證
  4. 在提供了經過單元測試的情況下,系統整合過程將會大大的簡化。開發人員可以將精力集中在單元之間的互動作用和全域性的功能實現上,而不會陷入充滿很多Bug的單元之中不能自拔。
  5. 使測試工作的效率發揮到最大化的關鍵在於選擇正確的測試策略,這包含了完全的單元測試的概念,以及對測試過程的良好的管理,還有適當的使用好工具來支援測試過程。

為什麼要進行單元測試

    單元測試是不是太浪費時間了?
  1. 不經過單元測試,直接進入整合測試,系統正常工作的可能性非常低,大量的時間被花費在跟蹤那些簡單的Bug上,會導致整合為一個系統時增加額外的工期。
  2. 編寫完整計劃的單元測試和編寫實際的程式碼所花費的精力大致相同。但是,一旦完成了這些單元測試工作,很多Bug將被糾正,在確信他們手頭擁有穩定可靠的部件的情況下,開發人員能夠進行更高效的系統整合工作,這才是真正意義上的進步。
  3. 除錯人員的不受控和散漫的工作方式只會花費更多的時間而取得很少的好處。
    單元測試僅僅是為了證明這些程式碼作了什麼嗎?
  1. 這是那些沒有首先為每個單元編寫一個詳細設計文件而直接跳到編碼階段的開發人員提出的一條普遍的抱怨。這樣的測試完全基於已經寫好的程式碼,這無法證明任何事情。
  2. 單元測試基於詳細設計文件,這樣的測試可以找到更多的程式碼錯誤,甚至是詳細設計的錯誤。
  3. 因此,高質量的單元測試需要高質量的詳細設計文件。
    我是一個很棒的程式設計師,是不是可以不進行單元測試呢?
  1. 每個人都可能犯錯誤。
  2. 真正的完整的系統往往是非常複雜的,不能寄希望於沒有進行廣泛的測試和Bug修改過程就可以正常工作。
    有整合測試就夠了,整合測試將會抓住所有的Bug
  1. 系統規模愈來愈大,複雜度愈來愈高,沒有單元測試,開發人員很可能會花費大量的時間僅僅是為了使該系統能夠執行。
  2. 任何實際的測試方案都無法執行。
  3. 在系統整合階段,對單元功能全面測試的負載程度遠遠的超過獨立進行的單元測試過程。
  4. 最後的結果是測試將無法達到它所應該有的全面性,一些缺陷將被遺漏,並且很多Bug將被忽略過去。
    單元測試的成本效率不高?
  1. 無論什麼時候做出修改都要進行完整的迴歸測試。
  2. 在生命週期中儘早的對產品進行測試將使效率和質量得到最好的保證
  3. Bug修改越晚,費用就越高,單元測試是一個在早期抓住Bug的機會。
  4. 相比後階段的測試,單元測試的建立更簡單,維護更容易,並且可以更方便的進行重複。
  5. 從全程的測試費用來考慮,相比複雜且曠日持久的整合測試,或是不穩定的系統,單元測試所需的費用是最低的。

單元測試的策略

  1. 單元測試最核心的並不是工具的使用,而是測試的策略和方法。
  2. 工具也好,實現手段也好,只是開展單元測試的必需品。
  3. 以下是單元測試常採用的策略:
    1. 哪些是重點模組?
    2. 哪些程式是最複雜、最容易出錯的?
    3. 哪些程式是相對獨立,應當提前測試的?
    4. 哪些程式最容易擴散錯誤?
    5. 哪些程式是開發者最沒有信心的?
    6. 80-20原則:80%的缺陷聚集在20%的模組中,經常出錯的模組改錯後還會經常出錯,這種應該列入測試重點。

測試用例的設計

  1. 單元測試的核心是測試用例的設計,測試用例的核心是測試資料的設計。
  2. 測試用例的設計主要從兩方面考慮:
    1. 功能。我們為了驗證一個函式是否實現了它的功能,通常採用黑盒測試的方法。常用的方法有邊界值、等價類、因果圖
    2. 邏輯結構。通常採用白盒測試的方法。常用的方法有判定覆蓋、條件覆蓋、條件判定組合覆蓋

TDD與單元測試的區別

  1. TDD是一種設計手段,而不僅僅是測試手段
  2. TDD的原則是“只做必要的事,不做多餘的事”。 
  3. TDD其實並不是單純強調測試,它首先是需求分析和設計技術。

面臨的問題

  1. 測試用例不全面
  2. 測試資料依賴於整個系統環境,不能迴歸
  3. 對於複雜的函式或介面的測試沒有實現
  4. 沒有建立測試基礎類
  5. 系統更新變化快,用例不能及時更新
  6. 測試函式的功能不唯一

相關文章