測試驅動開發(TDD)總結——原理篇

lyning發表於2019-02-21

我是一名喜歡追求高質量程式碼和高效率工作的軟體開發工程師,因此我學習 SOLID 和 Simple Design 等原則、閱讀優秀的開原始碼、閱讀相關的書籍、學習軟體過程方法和真實專案實踐,但是在追求高質量程式碼的道路上,總感覺目前的知識還不能幫我塑造成一種思維框架。在 2018 年年初機緣巧合閱讀了TDD(測試驅動開發)培訓錄這篇文章,瞬間欣喜若狂!

到現在接觸 TDD 將近一年,期間因為沉不下心只閱讀了很少的資料就在專案中實踐了一段時間,得到的效果還不錯,自以為已經很理解 TDD 實踐和背後的思想,結果在不斷閱讀相關書籍和關於一些 TDD 的討論中不斷暴露自己的無知,發現自己就像站在“達克效應”曲線的愚昧之巔,原來大部分自認為正確的知識都是不準確甚至是錯誤的。不過我很享受這種過程,在學習的過程中不斷驗證自己的知識是非常有趣的,這使我變得更有自知之明的同時也在不斷突破自身的認知上限。

接下來我會通過圖文的方式總結這段時間來對 TDD 的實踐和思考,以便於沉澱自身對 TDD 的理解,希望對讀者有所幫助,也希望讀者可以指點一二,集思廣益才能離真相更進一步。

範圍

TDD (Test Driven Development) 在不同的圈子、不同的角色的認知中可能會有不同的理解,有人可能會理解成 ATDD(Acceptance Test Driven Development),也有人可能會理解成 UTDD(Unit Test Driven Development),為了避免產生歧義,文章涉及到 TDD 專指 UTDD(Acceptance Test Driven Development),即 「單元測試驅動開發」

什麼是 TDD

以前很片面的認為 TDD = XP 的測試優先原則 + 重構,認為 TDD 只是通過單元測試來推動程式碼的編寫,然後通過重構來優化程式的內部結構。這很容易被理解成只需要先寫單元測試就可以驅動出高質量的程式碼,直到我精讀 Kent Beck 的著作《測試驅動開發》和不斷實踐思考之後才總算窺探到 TDD 藏在冰山下的面貌:

Kent Beck:“測試驅動開發不是一種測試技術。它是一種分析技術、設計技術,更是一種組織所有開發活動的技術”。

分析技術: 體現在對問題域的分析,當問題還沒有被分解成一個個可操作的任務時,分析技術就派上用場,例如需求分析、任務拆分和任務規劃等,《例項化需求》這本書可以給予一定的幫助作用。

設計技術: 測試驅動程式碼的設計和功能的實現,然後驅動程式碼的再設計和重構,在持續細微的反饋中改善程式碼。

組織所有開發活動的技術: TDD 很好地組織了測試、開發和重構活動,但又不僅限於此,比如實施 TDD 的前置活動包括需求分析、任務拆分和規劃活動,這使得 TDD 具有非常好的擴充套件性。

TDD 的目標

Kent Beck 在他的著作《Test-Driven Development》一書中提到:“程式碼簡潔可用這句言簡意賅的話,正是 TDD 所追求的目標”。

對於如何保證“程式碼簡潔可用”可以使用分而治之的方法,先達到“可用”目標,再追求“簡潔”目標。

可用: 保證程式碼通過自動化測試。

程式碼簡潔: 在不同階段人們對簡潔的理解程度也不一樣,不過遵循的原則差不多,例如 OOD 的 SOLID 原則,Kent Beck 的 Simple Design 原則等。

雖然有很多因素妨礙我們得到整潔的程式碼,甚至可用的程式碼,無需徵求太多意見,只需要採用 TDD 的開發方式來驅動出簡潔可用的程式碼。

TDD 的規則

在 TDD 的過程中,需要遵循兩條簡單的規則:

  1. 僅在自動測試失敗時才編寫新程式碼
  2. 消除重複設計(去除不必要的依賴關係),優化設計結構(逐漸使程式碼一般化)

第一條規則的言下之意是每次只編寫剛剛好使測試通過的程式碼,並且只在測試執行失敗的時候才編寫新的程式碼,因為每次增加的程式碼少,即使有問題定位起來也非常快,確保我們可以遵循小步快跑的節奏;第二條規則就是讓小步快跑更加踏實,在自動化測試的支撐下,通過重構環節消除程式碼的壞味道來避免程式碼日漸腐爛,為接下來編碼打造一個舒適的環境

關注點分離是這兩條規則隱含的另一個非常重要的原則。其表達的含義指在編碼階段先達到程式碼“可用”的目標,在重構階段再追求“簡潔”目標,每次只關注一件事!!!

TDD 的口號

測試驅動開發(TDD)總結——原理篇

簡單來說,不可執行/可執行/重構——這正是測試驅動開發的口號,也是 TDD 的核心。在這個閉環中,每一個階段的輸出都會成為下一階段的輸入。

  1. 不可執行——寫一個功能最小完備的單元測試,並使得該單元測試編譯失敗。
  2. 可執行——快速編寫剛剛好使測試通過的程式碼,不需要考慮太多,甚至可以使用一些不合理的方法。
  3. 重構——消除剛剛編碼過程引入的重複設計,優化設計結構。

假設這樣的開發方式是可能的,那我採用 TDD 真正的動機是什麼?

採用 TDD 的動機

  • 控制程式設計過程中的憂慮感。

有一個有趣的想象,當我感覺壓力越大,自身就越不想去做足夠多的測試。當知道自己做的測試不夠時,就會增加自身的壓力,因為我擔心自己寫的程式碼有 BUG,對自己編寫的程式碼不夠自信,這是一種心態上的變化。此時測試是開發人員的試金石,可以將對壓力的恐懼變為平日的瑣事,採用自動化測試,就有機會選擇恐懼的程度。

  • 把控程式設計過程中的反饋與決策之間的差距。

如果我做了一週的規劃,並且量化成一個個可操作的任務寫到 to-do list,然後使用測試驅動編碼,把完成的任務像這樣劃掉,那麼我的工作目標將變得非常清晰,因為我明確工期,明確待辦事項,明確難點,可以在持續細微的反饋中有意識地做一些適當的調整,比如新增新的任務,刪除冗餘的測試;還有一點更加讓人振奮,我可以知道我大概什麼時候可以完工。專案經理對軟體開發進度可以更精確的把握。

TDD 的整體流程

測試驅動開發(TDD)總結——原理篇

  • 想一下我要做什麼,想想如何測試它,然後寫一個小測試。思考所需的類、介面、輸入和輸出。
  • 編寫足夠的程式碼使測試失敗(明確失敗總比模模糊糊的感覺要好)。
  • 編寫剛剛好使測試通過的程式碼(保證之前編寫的測試也需要通過)。
  • 執行並觀察所有測試。如果沒有通過,則現在解決它,錯誤只會落在新加入的程式碼中。
  • 如果有任何重複的邏輯或無法解釋的程式碼,重構可以消除重複並提高表達能力(減少耦合,增加內聚力)。
  • 再次執行測試驗證重構是否引入新的錯誤。如果沒有通過,很可能是在重構時犯了一些錯誤,需要立即修復並重新執行,直到所有測試通過。
  • 重複上述步驟,直到找不到更多驅動編寫新程式碼的測試。

使測試程式可執行的三條策略:

  1. 偽實現——可以返回一個常量或變數,然後調整偽實現,直至偽實現變成可接受的實現程式碼。
  2. 明顯實現——直接將實現程式碼鍵入,因為已經明確如何編寫實現程式碼。
  3. 三角法——當我明確輸入和輸出但卻不知道它背後的設計和實現是什麼時,可以使用三角法,原理是先用簡單的可執行的例子作為參考的資訊源,然後推出測試的明顯實現。詳細資訊在參考資源中給出。

這三條規則的目的是達到程式碼的“可用”目標,只需要鍵入我們認為正確的程式碼使測試程式儘快通過即可。

TDD 的難點

  1. 缺乏軟體質量意識
  2. 缺乏一定程度的程式設計能力,很難設計出高內聚低耦合、意圖清晰的結構和程式碼。
  3. 缺乏分析需求並進行任務分解和規劃的能力,很容易在還沒開始 TDD 的時候就被打亂了節奏。
  4. 缺乏合適的測試環境和測試規範。
  5. 測試優先的習慣難以養成。
  6. 重構手法不熟練。

TDD 疑問

  • 都說小步快跑,具體步伐是多小?

無論是測試程式覆蓋的範圍還是重構時的中間步驟,TDD 建議是採用儘量小的步伐(測試無法再拆分,微小的重構),但是也沒有強制一定按照這種步伐,不同人的步伐可以不同,可以在實踐中不斷尋找適合自己的步伐,但是前提必須儘量小。

  • 什麼需要測試?什麼不需要測試?

除了那些不寫測試還能對自己的程式碼感到非常自信的人之外,這取決於自己的經驗和對程式碼的信心程度。如果某些程式碼自己認為即使不需要測試,執行和重構時也非常有信心,就可以不需要測試,比如大部分 set get ;相反,如果去掉會讓自己感到不安,就需要考慮加入測試。

  • 為什麼需要遵循不可執行/可執行/重構這個順序,不可以採用其它順序嗎?

這個問題《測試驅動開發》作者 Kent Beck 也很難去證明,因為沒有專門的人真正去做過這個統計,所以他表示不否認可能存在一些更好的順序設計。

  • 為什麼每次在可執行階段只編寫“可用”程式碼?

因為要儘快使測試執行起來,這樣可以降低來自系統的反饋週期,如果能夠快速持續得到來自系統的反饋,那麼就可以持續保持小步快跑的節奏。如果可以短時間實現一個好的設計,寫出優雅簡潔的程式碼,那麼在一開始 TDD 的時候,就應該採用最好的設計,因為這樣的效率會比較高。

  • TDD 是銀彈嗎?

TDD 不是銀彈,遇到問題需要尋找核心痛點是什麼,然後再對症下藥。

單元測試

與 ATDD 不同, UTDD 主要面向的是開發人員,所以 UTDD 在這裡主要關注的是軟體內部的質量屬性,如果說軟體的外部質量體現在“缺陷數”和“缺陷率”等指標,那軟體的內部質量屬性體現在程式碼的“可測試性”、“可讀性”和“可擴充套件性”等,這些幾乎是每一位軟體開發工程師的追求。“單元測試”作為 TDD 的產物之一,為了把控軟體內部的質量屬性,通常會使用到自動化“單元測試”作為軟體質量保證的“根基”。

在計算機程式設計中,單元測試(英語:Unit Testing),通常由軟體開發人員編寫,用於確保他們所寫的程式碼匹配軟體需求和遵循開發目標,是針對程式模組(軟體設計的最小單位)來進行正確性檢驗的測試工作。

每個理想的測試案例獨立於其它案例;為測試時隔離模組,經常使用 stubs、mock 或 fake 等測試馬甲程式。

通常來說,程式設計師每修改一次程式就會進行最少一次單元測試,在編寫程式的前後很可能要進行多次單元測試,以證實程式達到軟體規格書要求的工作目標。

從維基百科的描述中可以看出單元測試擁有如下特點

  1. 開發人員編寫。
  2. 測試函式/方法(TDD 的粒度更小)。
  3. 用於對程式碼進行正確性檢驗。
  4. 編碼前後和修改程式碼時都會執行單元測試。
  5. 經常使用 stubs、mock 或 fake 等測試馬甲程式確保每一個單元測試之間互相獨立(正交)

這裡從我個人的角度對單元測試進行簡單的分析,但對“單元測試”的理解,或者是它所處的位置還是不夠清晰,所以接下來我使用了“測試金字塔”模型來幫助我站在一個更高的視角理解“單元測試”。

測試金字塔

測試金字塔

上圖的“測試金字塔”模型按照執行速度和投入成本兩個維度對不同階段的測試工作進行非常直觀的視覺化,可以看到單元測試是位於“測試金字塔”的最底部,很明顯“單元測試”相對於其它不同階段的測試工作,擁有速度快(執行效率),成本低(維護成本)的優勢,同時也是作為上層測試工作的支撐,體現了“單元測試”的重要程度。

總結

文章純理論總結了 TDD 的全貌和一些 TDD 實踐過程中的策略,包括 TDD 的難點和疑問,文章多次提到“反饋”一詞是因為 TDD 是一種引入大量底層反饋的技術(得益於自動化測試),這些反饋使得很快就能看到行動的結果,使用 TDD,它將會在實踐的過程中學會如何雕琢我們的程式碼,從而得到穩定的物件導向設計、可維護和高質量的系統。

後續

紙上得來終覺淺,唯有知行合一,通過理論指導實踐,在實踐中不斷總結經驗,不斷驗證自己的知識,才能不斷對 TDD 有更深入更正確的理解。接下來將計劃出幾篇文章演示使用 TDD 如何解決一些真實的案例的總結,以便於提高自己的 TDD 技藝。

文獻參考


歡迎關注我的微信訂閱號,我將會持續輸出更多技術文章,希望我們可以互相學習。

測試驅動開發(TDD)總結——原理篇

相關文章