【TDD】課堂心得與筆記 - Day 3

91's 『自動測試與 TDD 實務開發(使用C#) 第八梯』 第三天課程筆記與心得。課程主題為 TDD、BDD、和團隊協同合作。

一、作業回顧

前言

        第二天作業是哈利波特書本特價,詳細需求請參考連結

        91 逐一針對我們的簽入逐條給予 comments,超有用的。我最大的問題是一開始 API 設計就很糟糕,這建議意思就是打掉重練。

        哈利波特這個 kata,著重於演算法,果然一堆人的實作方法都不一樣。給予全班的建議,除了 API 設計外,還有命名與演算法的易讀性。

 

API 設計要點

        第一個紅燈時,就要思考該如何呼叫這個 API,著眼眼前這個最簡單的需求,以 domain 為主,抽象化並給予好的命名。考量怎麼使用,API 才具有易用性,讓呼叫端用簡潔且清楚的叫用。譬如沒買的書,很多人呼叫端要傳某集買了 0 本,很不合理,為什麼買了第一集,第二三四五集要傳入 0,如果未來出了第六集,我是不是要修改所有的測試程式,多傳第六本購買狀態。

 

常見問題

  1. 命名還是一樣遭。
  2. 別想太多,就算 hard code 也可以,快速通過眼前的測試。
  3. hard code 寫多了要重構,事不過三,兩次就要改,將 hard code 與重複程式碼重構掉。
  4. 基本上到第二次紅燈,就應該看的出來演算法主軸是「購買數量 * 單價 * 折扣」,先滿足主軸,再追求可讀性與彈性,最後才是追求演算法效能。
  5. 錯誤的重構,有些人將 if 條件與內容封裝到 private function 中,但還是保留一堆 if else,這樣還是不好閱讀。
  6. 演算法最具挑戰的地方,是第六個測試個案有無折扣混合情況,第七個測試個案是測試多次折扣(引入迴圈或遞迴),程式碼瞬間會變髒,等綠燈後重構演算法又是另外一個挑戰。如果前面有即時重構,有抓住演算法主軸,那最後兩個測試個案會比較好寫,你會清楚知道異動哪處程式就好。

 

作業心得

        作業很重要一定要寫,主要價值有 91 親自幫你 code review(羞辱你),課堂上又花了一個小時講解如何透過測試去設計你的 API,如何透過測試驅動出 production code。寫過後看到 91 提供的 sample code,看到同堂課其他同學寫的程式,你才會知道自己像是井底之蛙。很多人不習慣先寫測試,或者毫無頭緒不知道如何起手,但看到第一個測試案例是「買一本書的價格」,你還會不清楚怎麼寫嗎?基本上只有 API 設計好壞的問題,雖然設計很醜(我的就是這樣),但按部就班依據測試個案慢慢寫,還是能寫出會動的程式。這也是 TDD 帶給我的好處。當你毫無頭緒時,流程很簡單,抓住最簡單的測試案例,先寫測試,然後寫剛好通過這測試的 production code,然後即時重構調整程式。

 

二、重構解耦合技巧 - Extract and Override

前言

        這技巧 91 於第一天最後的挑戰題目就演示過這個技巧,第二天下課前也預告第三天會特別講解這招。Extract and Override 也出現在《單元測試的藝術 第 3.4.6 章》《修改代碼的藝術 第 25.6 ~ 25.8 章》,是很多大師推薦對付 Legacy Code 的一大妙招。

 

使用時機

        看到 SUT 中有很難解耦合的相依物件,又因為 Legacy Code 問題導致很難抽取介面和依賴注入,都可以擷取(Extract)相依部分,然後利用繼承與覆寫(Override)抽換,把不可控的相依物件改成可控的假物件。

 

使用方法

        於 SUT 中:

  • 將相依物件從 SUT 中擷取(Extract)出來,改成 protected virtual
  • SUT 中使用擷取出來的方法。

        於測試專案中:

  • 建立新的 stub 類別,繼承 SUT 類別。
  • 覆寫(override )方法,並實作覆寫內容。
  • 打開 DI 的接口(建構子或屬性注入都可以)。

        於測試程式中:

  • 呼叫新建立的類別。
  • 將假的物件注入至新的類別。

 

分析

        使用方法中的步驟是我個人整理的流程,但沒自己寫過,死背下步驟也沒用,遇到了還是不會使用。此方法很簡單但卻很強大,讓你不用建立介面與打開 DI 的接口,快速完成 Unit Test。除了適用很難破除相依物件的 Legacy Code 上,也適用相依於底層的 SUT,譬如 new DateTime(); ,都能使用這招。

        除了特殊狀況,建議還是乖乖開 Interface 與 DI,做好設計才是正解。

        Extract and Override 有以下幾個問題:

  • private 改成 protected virtual,破壞封裝原則

                可測試性與封裝本來就會有些衝突,自行在兩者間取得平衡吧。

  • 手刻一堆只用於測試的類別

                可用 moq (於 c# 上常使用的 mock framework)套件,就不用手刻了,這是 NSub 做不到的功能。

  • 無法用於 staticsealed

                沒辦法,請自己考量是否能移除這兩個關鍵字或使用其他解耦合技巧,通常是使用其他方法解耦合。

  • 被覆寫的方法不會被測試涵蓋到,測試涵蓋率會下降

                可用整合測試,測試被覆寫的方法。

        建議,這招只是讓你快速解耦合並套上 Unit Test,當有測試保護後,建議還是去建立 Interface 和使用 DI 技巧。

 

參考資料

        詳細作法可以參考 91 寫的 [Unit Test Tricks] Extract and Override

        另外,強烈建議觀看以下影片,三部影片是連貫的,是 91 今年 2 月於 「AHA 線上面對面」活動中,講解 Isolated unit test 時,有講到 Extract and Override。

註:影片為 91 所有,若其他人有需要轉用或引用影片,請詢問 91。本次引用影片已經徵求 91 同意。

 

三、測試驅動開發(Test-Driven Development, TDD)

前言

        結果到第三天才開始講什麼是 TDD,原因是學習 TDD 前有很多前置作業,91 前兩天課程都在幫我們打底練內功(還有一塊內功是課前就應該要會的物件導向)。很多人都以為 TDD 是先寫測試再寫程式,聽完 91 的課才知道可以從很多不同面向去看 TDD。

 

The three laws of TDD

        TDD 不是什麼新名詞,至少書中記載 15 ~ 20 年前就有這個觀念。Uncle Bob 給 TDD 三大法則,遵守這三個法則,能讓你享有 TDD 所帶來的優點。以下提供三大法則原文與我流翻譯。

  1. 第一法則:You are not allowed to write any production code unless it is to make a failing unit test pass.(沒有紅燈不能寫 production code)
  2. 第二法則:You are not allowed to write any more of a unit test than is sufficient to fail; and compilation failures are failures.(一次只寫一個紅燈測試程式)
  3. 第三法則:You are not allowed to write any more production code than is sufficient to pass the one failing unit test.(用最簡單的方式讓紅燈變綠燈)

        意思再簡化就是測試程式(紅燈)與 production code(綠燈)之間交互循環。

        91 第一天課程就有說到,很多人都以為 TDD 的 T 只是測試,更準確說法應該是用測試程式來描述需求。如果改用需求面來看這三條法則,感覺會更不一樣。

  1. 第一法則 - 目的:用一個失敗的測試案例(需求)來說明為什麼要動 production code。
  2. 第二法則 - 專注:當紅燈(包含編譯失敗)時,別加下一個測試程式,先解決這個紅燈。
  3. 第三法則 - 剛好:Baby Step,從紅燈到綠燈,與測試程式(需求)無關的程式,一行都不多寫。

        訂定目標,用最簡單的方式,專注達成目的,這就是 TDD 的精神。

        為什麼要從需求面來解讀這三條法則?91 一直強調,用測試描述需求,針對需求開發,TDD 的測試程式和開發流程是輔助我們達成目標(需求)的方法。從需求面來看這三條法則,更佳深刻了解每一條法則的含意。

 

The Cycles of TDD

        TDD 開發是有規律的,是「紅燈 - 綠燈 - 重構」周而復始的循環。

  • 紅燈:將需求拆分成測試案例。這也是 TDD 入門第一道關卡,這是一門很重要的學問,學習「探索需求找出測試案例」和「排序測試案例開發順序」。
    • 探索需求轉換成測試案例:方法是先拿出紙筆,隨手將想到的測試案例寫出來或畫出來,關閉電腦螢幕,專心於腦內思考和紙筆記錄。我會給這個探索訂個時間,時間到了放下紙筆停止思考。若思考過程中發現需求不清楚地方,記錄下來待思考時間過後。去找 PO 或 SA 再次確認需求。
    • 排序測試案例開發順序:好的順序帶你上天堂,你會發現每個環節環環相扣,每個環節帶動下個環節。基本排序方式為從簡單的做起,但實務上常遇到每個測試個案看起來好像都差不多的感覺。學習排序只能多想多練習,或參考 91 今年年初用 TDD 寫 LeetcodeCodewars 文章,看幾篇就會知道排定開發順序超重要的,化繁為簡,快速且有節奏的開發。
  • 綠燈:只為了滿足紅燈的測試案例,用最簡單最笨的方法通過測試。如果你有好多種通過測試個案的寫法,請選擇最簡單的那種寫法(剃刀原則)。如同敏捷精神,先把腦中想到最簡單的實現方法寫成程式,然後才能快速改進。
  • 重構:察看 production codeunit test,依據 code smell,看程式碼有沒有需要調整的地方。
    • 大家都知道程式碼越少越簡單,程式越好重構,所以每次從紅燈轉到綠燈,一定要停下來審視程式碼,即時重構,寫多了就不好重構了。就算程式碼很簡單,也要有停下來頓一下的感覺,千萬不要以為太簡單而忽略了停頓審視的動作。
    • 當你一直寫程式,每 10 分鐘、20 分鐘、30 分鐘 ... ,跑過好多次 TDD 迴圈,當程式碼越寫越多,停下來審視程式碼,從完整功能、模組、系統邊界、或架構上去看,是否有 code smell?是否符合 clean code?是否符合簡單設計?
    • 一次只針對一個問題下去重構,切記不能邊重構邊加需求。
    • 即時重構能幫助你未來不會陷入泥沼裡,即時重構能加速未來開發速度。
    • 設計模式、演算法、效能調教、模組或架構層級的調整... 等,都是因為有測試程式保護下,讓你放膽去重構。

 

TDD is Dead?

        詳細流程請參考 91 整理的[懶人包]DHH: TDD is dead. Long live testing. 懶人包整理​。重點在於 Kent Beck 回覆 DHH 的 RIP TDD,利用反諷方式提出程式開發上有哪些問題,而 TDD 剛好能幫助解決這些問題。

  • Over-engineering.(過度設計)

                利用 TDD,專注每次撰寫的 production code 其功能是你想要的,只滿足你想要的需求,防止過度設計。

  • API feedback.(改善 API 設計與可用性)

                思考測試程式如何呼叫 API,從使用端的角度去思考,驅動出 API 介面。站在使用者如何使用 API 的角度上,開發出來的 API 才會漂亮,可用性才高。

  • Logic errors.(邏輯錯誤)

                需求和想的不一樣,想的和寫的不一樣。想的不夠全面,或者想太多。不管怎麼情況,利用 TDD 的測試程式釐清需求,藉由測試程式思考要實作的需求。確保想的和寫的一致。

  • Documentation.(撰寫文件)

                如何使用 API?如何記錄開發中的想法?維護文件是很痛苦的事,但維護人員又討厭沒有文件的系統。利用 TDD 的測試程式碼來記錄 production code,讓維護人員閱讀測試程式有如閱讀文件一般,也能確保程式與文件不同步問題。

  • Feeling overwhelmed.(不知如何開始)

                功能越複雜,往往看的需求發呆思考,思考下一步該如何切入,如何開始寫程式。萬事起頭難,寫程式也一樣。TDD 告訴你從最簡單最容易的需求下手,先寫程式,其餘功能暫時別想。讓你快速先寫出第一版程式再迭代改善。如果是連第一個最簡單的測試案例都想不到,可能是不了解需求或需求太大,建議去找 PO / PM 溝通釐清需求並將需求切小一點。

  • Separate interface from implementation thinking.(抽象設計與思考)

                為了可測試,為了 Isolated unit test,這些都能透過 TDD 來達成。利用 TDD 驅動出 SOL(L)ID,驅動出 CQRS,只能讓 SUT 功能單純單一或者倚賴抽象的介面。TDD 強迫你思考,不抽象不倚賴介面會很難寫測試,為了好寫測試自然就會做到抽象設計。

  • Agreement.(確保修改沒有問題)

                如何確保這次修改沒有問題?如何快速了解別人正在修改什麼?剛好 TDD 可以幫你,確保這次調整至少符合測試個案,藉由測試程式讓別人知道你正在處理什麼事情。

  • Anxiety.(防止改 A 壞 B)

                TDD 的測試程式剛好可以防止這個問題。

        TDD 剛好可以解決以上問題,若有其他方法可以解決以上問題,確實可以不使用 TDD 開發。若拿劍比喻 TDD,當你達到無劍勝有劍的境界,意思達到如同 DHH 或 Kent Beck 等級(可參考 Kent Beck 所寫的論點),有能力知道哪些要寫測試,哪些地方不用寫測試,但測試程式與 production code 還是寫的比大多數人好。在達到這樣境界以前,還是學習如何駕馭使用 TDD 這把劍吧,TDD 能幫助你解決問題的。

        DHH 也不是全部說錯,我認同 DHH 一句話,不要陷入基本教義派迷失,也不要認為 TDD 是萬靈丹。役物而不役於物,TDD 是個方法,剛好能解決 Kent Beck 遇到的 8 大問題,而這 8 大問題剛好是很多程式開發人員常遇到的痛點。如果你沒有這些痛點,或者你有一套自己的解決方法可以處理你遇到的問題,確實可以不用 TDD。

 

推薦參考資料

        看高手邊寫程式邊聽講解,是學習的好方法。以下兩篇從無到有,從需求分析到利用 TDD 實作程式,推薦給有興趣的人看看。

  1. 「Live Coding」第十期 TDD Hangman by 姚若舟(一)

  2. 「Live Coding」第十期 TDD Hangman by 姚若舟(二)

        建議使用自己熟悉的語言寫一次,像我也仿照寫了 C# 版,收穫很多。

 

四、簡單設計

前言

        我們常常會思考程式設計,該如何做好設計?可能會想 SOLID、每個 function/method 要 15 行以內、程式碼複雜度、...... 等。還是使用設計模式這鎚子,看到有問題的程式碼就敲下去?

        Kent Beck 於Extreme Programming Explained書中總結四條程式撰寫規範,稱之為 Simple Design,這四點也是程式撰寫上的基礎規範:

  • Runs all the tests(通過所有測試)
  • No Duplication(程式不重複)
  • Reveals intention(從程式碼可表達意圖)
  • Fewest elements(沒多餘功能)

 

Runs all the tests

        不管是單元測試、整合測試、或 end to end 測試,撰寫測試可以提早發現問題,快速找出 BUG。可以放心的迭代程式,不用擔心改壞其他功能,可以大膽重構,讓系統好維護,讓系統活的又長又久。切記,不要為了測試而測試,不要為了測試涵蓋率而測試,因時間有限,重要的地方用細顆粒測試,比較不重要的地方用粗顆粒測試。

 

No Duplication

        Don’t Repeat Yourself(DRY),重複的程式碼是 code smell 之首,也是工程師惰性的證明(誤)。重複的程式碼最大問題是不曉的在什麼地方存在相同的程式碼,需求異動時要花費很多時間盤點重複程式碼所在位置,讓你改很多地方,或者有些地方沒改到,不容易維護。重複的程式碼也代表該段程式除了複製貼上(BUG 也一併貼到新的地方)外,不容易複用。

        除了程式碼層級的不重複以外,功能層級去看,或模組層級去看,相似邏輯或類似功能也算是一種重複,可以用抽出共用方法或共用模組去解決功能重複的問題。

        此項也可以延伸其意義,意思是要讓程式可維護性高。

 

Reveals intention

        測試程式或 production code,要做到讓別人看程式碼就馬上知道這段程式碼想表達的意圖,實作了什麼需求。不管是文件還是註解,都有可能維護不佳或讀的人看不懂腦補。最好的作法是讓你的程式碼說話,讓維護人員用最少時間就能看懂程式碼。程式碼職責切割越乾淨,適時的抽象與封裝,好的命名都可以讓程式自我說明其功能。

        此項也可以延伸其意義,意思是要讓程式可讀性高。

 

Fewest elements

  • 剃刀原則:有很多解決方案,選用最簡單的那一個。
  • YAGNI(You aren't gonna need it.):撰寫程式時,思考你真的需要嗎?是否需要 DI Framework?要套用 XX 設計模式嗎?不要腦補東想西想,只要專注你現在要的功能進行設計就好,不要過度設計。
  • KISS(Keep It Simple and Stupid):不要讓你的程式太複雜,簡單就是美。保持簡短的介面、合適的參數物件、用最簡單直覺的方式實現功能。

        或許有人反駁,可能是 Domain 過於複雜,或者為了擴充性增加設計導致稍加複雜。確實很容易找出很多例外狀況來說明為什麼會有這些多餘的功能。程式開發人員職責之一是化繁為簡,找到平衡點是每個程式開發人員要去思考的事。

 

五、行為驅動開發(Behavior-Driven Development, BDD)

前言

        團隊協同合作時,是否遇到以下問題?

  • PO / SA 和開發人員雞同鴨講,沒有交集,互相敵對。
  • PO / SA 和開發人員間花費太多時間溝通。
  • 開發完才被告知「這不是使用者要的功能,不符合需求」。

        結果就變成下圖各做各的事,一個產品各自表述。

        當然,這個問題牽扯到團隊協同合作,需要一個能貫穿需求、程式開發(含測試程式)、與測試團隊的測試,整合各團隊的想法與思維,確保開發出來的系統是使用者想要的東西。而解決此問題的方法,正是現在要介紹的行為驅動開發(BDD)。

上圖擷取至 skilltree 本次課程講義,已經詢問 91 可於本篇 Blog 使用。

 

BDD - Behavior

        上圖顯示「需求」、「開發」、與「測試」,若沒有一組好方法整合各環節,光溝通開會討論的成本就相當的高。而 BDD 正式整合各環節的方法之一。使用人類講話方式描述需求與系統行為,可用來和團隊成員溝通形成共識,將這些共識與測試程式綁在一起,再藉由這些測試個案驅動開撰寫 production code,然後產生出規格文件。從需求到產品到文件,一氣呵成,一環扣一環。

        用人類講話方式描述需求案例與系統行為是很重要的,共同的語言,使用共通的模式,避免專業的傲慢,防止知識的詛咒。課堂使用的 BDD 工具是 specflow(c# 版的 cucumber),所以描述系統功能採用 gherkin

        BDD 第一步驟是將需求轉換成一個又一個案例,使用舉例的方式,如同說故事般,清楚的傳遞想法。在這步驟我們使用 gherkin 模式,格式大致如下:        

Feature: <title>
    In order to <value>
    As a <role>
    I want <feature>

    Scenario: <title1>
    Given <context>
    When <event>
    Then <outcome>

    Scenario: <title2>
    Given <context>
    When <event>
    Then <outcome>

        Feature 部分為將需求轉換為故事,利用 In order to ...As a ...I want ... 描述怎樣的角色在怎樣的情境下會有這個需求。Scenario 則是在這個故事底下有哪些主要的場景,利用 Given - When - Then 格式描述這個場景,對應到 3A 模式,Given 對應到 Arrange,When 對應到 Act,Then 對應到 Assert。用通用案例加法器舉例:

Feature: Add
	In order to avoid silly mistakes
	As a math idiot
	I want to be told the sum of two numbers

Scenario: Add two numbers
	Given I have entered 50 into the calculator
	And I have entered 70 into the calculator
	When I press add
	Then the result should be 120 on the screen

       Feature 清楚寫到功能的需求,而 Scenario 就將需求實例化,用一個又一個真實的案例來說明需求。需求配合案例的方式,用舉例方式來溝通,每一個 Scenario 就是測試個案,可以給開發人員驅動開發,而每一個 Scenario 就是未來驗收的項目,也就是所謂的驗收測試的依據。

        將使用者操作行為利用故事與實例來描述,一般會寫在小卡片或便利貼上,團隊成員會逐一討論卡片上的內容,若有不懂的地方就會請教 PO / SA。溝通釐清需求後,由開發人員認領卡片,依據卡片上的內容驅動開發程式碼,讓程式符合卡片上的內容,符合驗收測試的條件。用使用者操作行為驅動開發的方式,我們稱之為 BDD。

        如何寫好 Feature 和 Scenario?如何釐清需求?如何交付使用者想要的系統?推薦閱讀《Specification by Example 中文版:團隊如何交付正確的軟體​》

 

BDD - Driven Development

        當有了 Scenario,如何將這些文字轉換成程式碼呢?在 C# 中,就要使用到 specflow 這個套件。specflow 功用就是將 gherkin 格式轉換成測試程式,讓測試程式與需求繫結綁定,確保我們開發都是遵循這些 Scenario。如同官網寫的,Binding Business Requirements to .NET Code。讓需求就是測試程式,測試程式就是需求。

        specflow 語法細節就不多說了,課堂上 91 使用很多 Lab,藉由多次操作演練。簡單記錄幾個重要的點:

  • 設定:
    • 於 VS 的擴充功能中安裝 specflow(一次性設定)。
    • 於測試專案中,利用 NuGet 安裝 specflow。
    • specflow 預設使用 nunit,若要使用 MsTest,於測試專案上,修改 App.Config,於 <specFlow> 區段內,加上 <unitTestProvider name="MsTest" />
  • feature 檔:
    • 主要用途是用文字描述 domain know how。
    • 內容可以用中文,關鍵字部分,可在 feature 檔上加上 #language: zh-TW,轉換成中文。
    • 加入副檔名為 .feature 的檔案,檔案內由 feature 和 scenario 兩部分組成,feature 採用 user story 格式描述需求,scenario 是將需求實例化,採用 Given-When-Then 格式,將需求案例用步驟方式描述。
    • 用途是描述需求,故不要寫和頁面或行為實作相關內容。譬如寫「When 執行查詢」,不要寫成「When 按下確定按鈕並執行查詢」,不然調整頁面將按鈕文字從確定改成搜尋,又要回頭微調 feature 檔內容。
    • 每個 scenario 就是一個測試案例,測試案例間不要相依,都是獨立的。
    • 於 scenario 上按下右鍵,選擇 Generate Step Definitions,將文字需求轉換成測試程式,這些測試程式副檔名為 .step,稱之為 step 檔,我們將會在 step 檔內寫測試程式。
    • scenario 可以下中斷點偵錯。
    • scenario 中相同的 step,只有參數不同,可以使用 Scenario Outline / Examples。只需要把 scenario 上的 <column name> 對應到 Examples 裡面的欄位名稱即可。
    • scenario 上加上 Tag @Ignored 忽略不測試。
  • step 檔:
    • step 檔如同 unit test,用來撰寫測試程式。
    • feature 檔和 step 檔採用文字綁定,意思是名稱一樣,就把該需求描述與測試程式綁定成一對,譬如 feature 檔中寫到 When I press add,step 檔中會有 [When(@"I press add")],系統看到同名的就會自動綁定。因為 feature 檔會掃描全專案,找到名稱相同的 step 為止。當專案越來越大,有機率碰到因為同名的導致綁定到錯誤的測試程式。強烈建議產生 step 檔後,第一件事在 class 上面加上 [Scope(Feature=”xxx”)],限定這個 step 只能被 xxx.Feature 檔讀取到。
    • Scope 有三種範圍,從大至小分別為 Feature、Scenario、和 Tag 三種。
    • 在同一 Scope 下,相同的 Step 可以重複利用。
    • 因為會自動綁定,可用從 scenario 使用移至定義找到對應的 step。反之,於 step 上使用 Go to Specflow Step Definition  Usages 移至對應的Scenario。
    • Tag 是把常用的功能寫在 Hook 中,有哪些 scenario 要使用到相同功能,用 Tag 掛上去就好,很像 AOP 或 Action Filter。譬如每個頁面使用前要先登入,那我們就把登入功能寫在 Hook 中,假設命名為 @user,其他頁面要測試前,於 scenario 上掛上 @user 就好。Tag 功能很強大,也可用於初始化測試資料,譬如測試前與測試後都要清空資料,確保每次測試資料都一樣,此部分可參閱 91 寫的[Specflow] TRUNCATE Table Test Data by Tag
    • [BeforeScenario][AftreScenario] 很好用,前者會於跑每個 scenario 前執行,後者會於每個 scenario 後執行。

        職責分離,feature 檔用文字描述需求,將需求實例化。step 檔將每個需求實例轉換成測試程式碼。feature 將商業邏輯抽象,step 呈現商業邏輯具體操作步驟(行為)。因為是將使用者操作行為用文字寫成需求實例,所以不懂程式的人也能輕易看的懂。轉換成 step 後,能確保開發人員依據需求實例來開發,不然不會通過測試。BDD 和 end to end 相似,是從使用者行為面開始,所以實務上都是利用 BDD 撰寫 end to end 測試(web testing 和 整合測試)。

        還記得第二天的心得中寫到,撰寫 web testing 時會透過 FluentAutomation 建立 PageObject,讓測試程式抽象化。將 specflow 用於 web testing 上,一樣可以使用 FluentAutomation 與加墊 PageObject,讓 step 抽象化與隔離頁面,這樣要改頁面統一調整 PageObject 就好,不用動到 step。使用 BDD 做 web testing,可參考 91 寫的[Web Testing]驗證 table 內的多筆資料,淺顯易懂且用到很多不錯的技巧唷。

        BDD 其流程也是遵守紅燈-綠燈-重構流程,所以 step 內的測試程式也要重構。共用部分大部分會拉到 [BeforeScenario] 或 [AftreScenario] 中,或者拉到 Hook 中,透過 Tag 達到重複使用共用程式。

 

BDD 流程

        一張圖總結上面流程。可以配合上面文字與下圖服用,我的建議是先去寫幾次程式後再來看,或邊寫程式邊看圖,感觸會更深。

上圖擷取至 skilltree 本次課程講義,已經詢問 91 可於本篇 Blog 使用。

 

BDD 總結

        BDD 是利用需求驅動開發,確保你寫的程式符合使用者操作行為,符合使用者期待的需求,防止使用者要 A 結果給他 B。

  • Feature:抽象的商業邏輯。使用人話讓團隊凝聚對需求的共識與驗證方法。
  • Step:商業邏輯於實務上的操作步驟,初始化 SUT(包含準備資料),然後執行與驗證 SUT。

        切記,BDD 一定要配合 Unit Test,不然 BDD 紅燈會不知道那個環節出問題。實務開發上,利用 BDD 確保程式功能符合使用者需求,然後再使用 TDD 確保每個功能都能正常執行。用測試粒度粗的 BDD 包圍,再用測試粒度細的 TDD 實現技術細節。

        BDD 包含了整合測試,基本上不會特別去寫整合測試,除了以下狀況:

  • 模組間的溝通
  • 與第三方溝通
  • 與基礎建設(Infrastructure Layer)間溝通

        specflow 是 C# 上實現 BDD 的工具(別的語言 BDD 工具,可用關鍵字 Cucumber 加上語言名稱搜尋)。specflow 多用於 end to end 測試,所以第二天心得中的 web testing 內容都可以運用於 BDD 上。墊一層 PageObject 的用意?使用 FluentAutomation 或自己打造一層 Driver 的用意?讓 Step 更抽象的好處是什麼?如果思考後能說出答案,恭喜你,若不能請回頭看我第二篇的心得。

        實務使用 BDD 時,團隊會開會討論 feature 和 scenario,若有不清楚馬上詢問 PO / SA。當確定 scenario 後,透過 specflow 轉換成 step。撰寫 step 時,如同寫 unit test 一樣,要去思考和設計,從類別名稱、建構子、function / method Name、參數名稱、該如何呼叫 SUT、如何驗證功能... 等角度思考並設計出好的 production code。

        什麼是 BDD?用人話描述需求,用人話描述測試案例,用人話來寫程式。讓需求、程式、和測試案例一體化。彷彿給系統加上名為需求的邊界,防止過渡設計或寫出使用者不要的東西。        

 

六、曳光彈開發方式(Tracer Bullet Development, TBD)

        曳光彈是飛行中會有亮亮的尾巴的子彈,在光源不足或黑暗中顯示出彈道,協助射手修正彈道,甚至作為指引以及聯絡友軍攻擊方向與位置的方式與工具(以上文字擷取至 wiki)。

「Tracer Bullet」的圖片搜尋結果

 

        TBD 一詞出現於《軟體專案成功的管理之道,意旨開發流程先以快速建構雛型系統的方式,結合 Mock / Stub Object、Interface、hard code... 等方式,快速開發出一版雛形系統,藉由這套雛形系統與 PO / PM 確認方向是否正確?若方向正確就開始分工,每一步都是為了下一步演進,彷彿順的一條軌跡前進一般。TBD 產生的雛形和一般的 prototyping 不同,並不是用完就丟棄,TBD 的雛形是用來確認每次前進的方向是否正確,若有偏差就快速改正。經由一次又一次的改正,然後成為 production

        這和 TDD 開發如出一轍,實務 TDD 上,順的需求、驗收測試、到單元測試,最後寫成 production code,這和 TBD 那條發光的軌跡一樣,照亮開發的道路,確定要走的路,堅定的朝目標前進,防止迷失方向。這也是 91 在課堂上提到 TDD 與 BDD 和 TBD 精神相同,確定正確的方向,每一步都不會浪費,可以從現在這步往下一步開發。此部分去實作 91 提供的範例 Lab 13 就能深刻體會到這件事情。

        打造一個可行走的骨架,猶如拖的長長尾巴的曳光彈,照亮團隊前進的方向,指明應走的路。TDD 和 BDD 也呼應了 TBD 的精神,讓團隊少走冤望路。想進一步了解 TBD,可參考Tracer Bullets and PrototypesThe GROWS Diagram

 

七、實務開發流程 - BDD + TDD

        先稍微總結一下這三天主要學到的三大重點:

  • Unit Test:確保你想的和你寫的一樣
  • TDD:確保你先想清楚才動手
  • BDD:確保你想的是使用者要的

        實務開發上,上面三者缺一不可。我們學習是從 Unit Test 到 TDD 到 BDD,但實務開發時,順序是 BDD 到 TDD(內含 Unit Test)。其實原因很簡單,因為 BDD 是從需求出發,將需求與測試與 production code 綁在一起,彷彿曳光彈一樣,幫我們照亮要行徑的方向。確定前進的方向後,改採用 TDD,想清楚前進時的每個環節,確認需求細節後在動手開發。

        從 BDD 到 TDD,將需求拆分成小需求,每個小需求驅動出 production code。由上到下(Top down),由外到內(Outside in)的開發流程,感覺像一步一步包圍目標的感覺,從測試粒度粗的到測試粒度細的。當完成一輪後,會由下到上,由內到外,整個測試個案都變成綠燈。

        切記,每一輪 BDD 後也是要重構的,多輪 BDD 完成後,再回頭看整體程式與架構,看到問題請馬上重構。

        BDD 的 B(行為)所產生出來的 feature 和 scenario,是從需求產生出來的,意思這些項目也是使用者驗收的標準,所以 BDD 也能稱之為驗收測試驅動開發(Acceptance-Test-Driven development, ATDD)。使用 BDD(ATDD)與 TDD 交互驅動開發,這種模式首先出現在《Growing Object-Oriented Software Guided by Tests》一書中,所以又有人將這種開發模式稱之為 GOOS。GOOS 開發模式檢意圖如下圖所示。

資料來源:上圖取至於 Odd-e 曾經主辦的活動「GOOS 實戰 - 以驗收測試和單元測試驅動整個開發過程

        GOOS 是實務開發流程,開發前與開發後有還有一些(很多)步驟,提供 91 手繪流程,從需求到 SBE 到 ATDD 到 TDD 到 交付使用者驗收,整個流程完成才算是完成。

資料來源:91 於 GOOS 課程手繪海報,若有人想引用圖片,請詢問 91。

 

        因三天時間有限(三天塞了約為四天份內容唷),只能教整體流程各項目,無法帶過整個流程。整個 BDD + TDD(GOOS 流程),建議去上 91 的 GOOS 課程,標準的 GOOS 課程約 2 ~ 3 天(91 濃縮一天講完),跑過一輪會有打通任督二脈的感覺。

        我將各流程與關鍵重點彙整成一張圖,將實務開發流程結合這三天重點,可用於未來複習與記憶用。

八、團隊協作與導入重點

        進入 GOOS 流程前,在實際寫 code 前還有很多前置工作,像是 Impact Mapping、Story Mapping、Specification By Example(SBE)。每個環節內又有很多事要做,如下圖所示。

資料來源:網路上一堆版本,不確定最原始出處,此圖是 google 搜尋找到的。

 

        如果是敏捷團隊,上圖各環節有哪些角色協作,都已經決定好了。如果今天非敏捷團隊的開發人員想導入 GOOS 這種開發方式,勢必要導入 SBE。SBE 是團隊和 PO / SA 協同合作的產出,那該如何讓 PO / SA 寫 SBE 呢?

        非敏捷團隊,一般 PM / SA 職階都比開發團隊人員還高,年資也比較久,很難用命令的方式請他們寫 SBE 格式的使用者需求規格確認書。91 依據自己的經驗,提供了誘導方法,逐步導入。

  1. 團隊討論需求時,使用白板和便條紙或紙卡,將需求彙整成 SBE 格式,一邊整理一邊詢問 PO / SA 是否有理解錯誤。
  2. 將討論完的 SBE 電子化,產生出 scenario report 和 PO / SA 再次確認需求。
  3. 做出模型(hard code),再次和 PO / SA 確認是不是想要的東西。
  4. 分 user story,分工寫程式。
  5. 久而久之 PO / SA 習慣 SBE 的 Given-When-Then 格式,也習慣用這種格式和開發團隊討論需求。
  6. 以後需求討論會議前,讓 PO / SA 產生初版 SBE,使用這種格式寫草稿來和團隊討論。

        91 提到,以前帶的團隊還有最後一步,將 SBE 簽入 github,PO / SA 只要去 github 上新增 / 修改 SBE,完成後 Pull Requests,團隊就會看到新增或異動的需求,團隊開會討論確定需求後,由資深同仁確認簽入。

        另外一個重點,不管是誰產生 SBE,都要由團隊主導,逐一確認,有問題馬上詢問 PO / SA,防止知識的詛咒

        91 另外說到,導入會遇到很多問題,但別一昧的怪東怪西,有時候導入的人也要反思,是不是導入的方法有問題,或導入真的對團隊有幫助嗎?學會一門技能很簡單,推廣到團隊才是困難的。你已經知道 BDD 和 TDD 的好處,如何將這份好影響其他人,拉取更多志同道合的朋友,一起採坑一起被雷一同歡笑,然後逐漸感染更多人加入。這樣可以推廣到一個或數個團隊,但要推廣到整個資訊中心,勢必要說服主管。主要推廣方式是看公司有什麼痛點,針對痛點去展示你的方案,進而讓主管認同。若你的團隊沒有痛點,大家對現行開發方式接受度高,那提升開發效率的方法是買符合人體工學的桌椅或更多的大螢幕或買 SSD 硬碟,而不是強制導入 TDD 或 BDD。就像螞蟻的故事,本來螞蟻做的好好的效率產能都很好(沒有痛點),硬要導入或更改流程或制度,只會造成反指標。

        相信會來上課的應該都是有碰到什麼痛點,想要改進所以才來上課的吧。思考在實務上該如何應用,可以解決怎樣的問題,和團隊有相同痛點的人分享,一同解決痛點是最有效的導入方法。

        如果有長官支持導入,導入也是有順序的,實務上雖然是 BDD 到 TDD 開發,但導入時要把團隊基本功拉起來,應從 OOP、OOD、DI、Unit Test、end to end Test、Refactoring、TDD、到 BDD,如同 91 上課教導的順序。打好底練好內功,實務上使用 BDD 和 TDD 才能水到渠成。

        導入碰到最大問題絕對是人,第一篇心得文有寫到 TDD 能有效降低開發成本,另外《The Art of Unit Testing with Examples in .NET》這本書寫到,開發時間變成兩倍,但整體開發生命週期是下降的。節錄書中比較表,如下圖所示。不過 91 近期都改口,使用 TDD,實際開發時間也會比較快,因為我們是使用地表最強的 IDE,配合 Resharper,只會比傳統開發更快,不會慢。會慢是因為剛開始導入團隊不熟悉或實力不夠(譬如物件導向基礎不好),多練習多寫自然會比傳統開發還快。91 說不服來者來單挑 XD。

資料來源:《The Art of Unit Testing with Examples in .NET》

 

九、Living Documentation

        有遇過辛苦寫出來的文件,結果幾個月後發現文件和程式對應不起來。當有緊急事件要處理時,你相信文件寫的內容嗎?工程師最討厭別人不寫文件不寫註解,也最討厭自己要去寫文件寫註解。維護文件是吃力的事,但也是不可避免的事。當你思考如何讓團隊製作的文件內容是正確的,活文件就是你要的解答。

        活文件這觀念我是看《Specification by Example 中文版:團隊如何交付正確的軟體​》這本書,要做到程式與文件一體化,只要修改其中一項,另外一項沒有修改,就會報錯。將 BDD 的 feature 檔內的 feature(user story)和 scenario (需求實例)轉化為文件。活文件內容是團隊與 PO / SA 討論的共識,讓文件與程式同步,這份文件才有參考價值。

        不管身在哪種團隊,寫文件絕對是避免不了的,敏捷團隊也是有需求概述與系統架構文件,必要時也要寫使用者操作手冊。最重要也最常改動的需求說明與實例化,這部分就靠工具直接與程式綁定,做到調整程式就能自動更新的活文件。達到測試即文件,文件即測試,讓你的文件是可執行的規格書。

        課堂使用工具為 Pickles,可參考使用 Pickles 搭配 SpecFlow 產生即時更新文件(living documentation)這篇,設定寫得非常詳細。

        很喜歡下面這張圖,從 BDD 到 Live Documentation,我們不是以文件為主體的開發,而是開發完會自然的產生文件。

資料來源:此圖為 google 搜尋找到的,原圖出自於《Specification by Example》這本書。

 

十、心得

        團隊要學習的要會的東西真的太多了,下圖學習地圖,這三天課程(上課時數接近四天)只有地圖最右邊那一塊,這也是開發人員主要的學習項目。學會這些開發方法與技巧後,最重要的下一步是持續整合(CI)。91 課堂上說到,導入 BDD 或 TDD 前,一定要有版本控管,讓開發人員能夠持續簽入與整合,然後導入自動化測試後,架設 CI Server 可以快速看到效果。

 

        三天下來,讓我感受最深的,就是萬事起頭都是需求。所以不管哪種測試驅動開發,都知道那不是單純的測試,是需求,是規格。91 課堂一直提醒,Not TESTING, is SPECIFYING.不是 Test First,是 Think First。未來不管出現什麼新名詞,只要抓住這個中心思想,就不會被名詞所迷惑。

        GOOS 這種開發流程,讓我深刻體會到 Top-Down 與 Outside-In 思考與設計,從上到下,從大到小,專注於需求,逐漸縮小範圍,刻畫出最終結果。過往開發拿著紙筆在腦中規劃與設計程式,然後實作,遇到頭腦打結,可能會花費過多時間在思考。現在變成依據 SBE,思考如何切分更小的需求,思考出幾個 case 卡關了,就先做這幾個簡單的,邊寫邊思考,逐步補充增加 case。別想太多,做了再說。先抓住眼前看到的東西,解決可以解決的問題,然後反覆迭代,逐步豐富程式內容。很喜歡這種聚焦當前需求,只為了解決眼前這一個問題的感覺。因為以前我真的會有影片的問題,為了修燈泡結果跑去修汽車。

        這堂課除了教導 TDD 和 BDD 以外,重點是如何設計出好的程式,如何藉由測試驅動出物件導向程式設計。私認為這是我來上這堂課最大也是最重要的收穫。那怕公司沒有使用 BDD 和 TDD,拿來私底下練功,就算回到公司不用測試驅動,也能自然寫出好測試低耦合且架構 OK 的程式。現在回頭去看作業 comments,全部都是精華,譬如 91 建議 production code 除了工廠或建構式外,盡量不要 new 物件,降低耦合,只有一個進入點可以 new,好測試也好管理,也符合開放封閉原則。

        回頭再寫一次 Lab 與自己的筆記,再看了 91 批改作業的 comments,真心覺得來上這堂課真好,再次釐清了概念。接下來,就是自己多練習了,唯有持續不斷的刻意練習,才能成長。每一次的練習,夾帶的思考與突破,同樣的題目不同的 API 設計,寫久了就越寫越快。