TDD-網球範例(Java) === [Toc] >情境描述: >>我們要設計一個盒子,上面有兩個按鈕代表左邊或右邊得分。 盒子會幫 tennis 的裁判記住分數,呈現分數結果,裁判只要決定那一邊得分,按左邊的按鈕或右邊的按鈕,螢幕就會呈現分數結果,裁判只要念出來就好。 因為 tennis 的分數結果是有其商業邏輯來決定的。 1. 0分代表 love, 1分代表 fifteen, 2分代表 thirty,3分代表 forty, 4分在一般情況下就是 win; 2. 平手會加上 all; 3分(含)以上則是特殊的平手,叫做 deuce 3. deuce 之後任何一邊要連得2分才算贏, deuce 之後得一分,叫做領先 "adv", 得兩分就是 "win” #### 前置作業: 開一個新的maven專案,先去 pom 檔加 junit5 跟 assertJ 的 dependency。 去 plugins 裡面增加 Custom Postfix Templates。 ## 常用快捷鍵 | 用處 | Windows版快捷鍵 | Mac版快捷鍵 | | --- | --- | --- | | new 一個 class / 一個package /generate | alt + insert | command + N | |跳到錯誤的地方|shift + F2| fn + F2 | |**compile系列**|| |全部測試去compile的快捷|Ctrl鍵 (⌃) + R| Ctrl鍵 (⌃) + R | |跑上次的compile|Ctrl鍵 + Shift 鍵 + F10| Ctrl鍵 (⌃) + Shift 鍵 (⇧) +R | |打開選擇配置視窗,然後選擇要執行的單元測試配置| Alt + Shift + F10 | |**Introduce 系列**|| |Introduce field| Ctrl + Alt + F | alt + command + F | |Introduce parameter | Ctrl + Alt + P | alt + command + P | |Introduce constant | Ctrl + Alt + C | alt + command + C | 其餘參考:https://forum.gamer.com.tw/Co.php?bsn=60292&sn=15617 ### ==秉持原則:測試情境->出錯->測過、得到預期->重構、優化== ## 大致步驟 因為是要去建立一個計分器,所以他必須要符合每一種情況。 所以我們在建立的時候的步驟,會先從只考慮一位球員的得分情況去建立功能。 再只考慮另一位球員的得分情況,最後才是兩人平手、平手後領先,以及最終獲勝的情況。 ## 創建類別與方法 test上面new一個package,再new一個class 快捷鍵為 Alt + insert ![螢幕擷取畫面 2023-12-07 144207](https://hackmd.io/_uploads/HkIaFJyU6.png) 先在test的class中generate一個test method generate快捷鍵一樣是 Alt + insert ![螢幕擷取畫面 2023-12-07 144553](https://hackmd.io/_uploads/BJQD9yJLT.png) test method的名字取為love_all(因為為零比零情況的方法測試) ![螢幕擷取畫面 2023-12-07 144732](https://hackmd.io/_uploads/ryQA9JyUp.png) ## 添加方法的行為 ### new Tennis() new一個Tennis方法:打new Tennis().var 按 enter 即可 ![螢幕擷取畫面 2023-12-07 150008](https://hackmd.io/_uploads/rkAjTJy8p.png) ### 除錯建立類別 因為我們還沒有Tennis方法,所以會報錯。 那除錯的快捷鍵為 F2,藉由除錯意見去建立Tennis class。 ![螢幕擷取畫面 2023-12-07 153850](https://hackmd.io/_uploads/ByTo8g1Lp.png) ### 增加得分行為 然後再回到TennisTest(按alt + 左右鍵變換分頁), 在love_all裡面增加得分行為,用Assertions EqualTo的方法。 ![螢幕擷取畫面 2023-12-07 153624](https://hackmd.io/_uploads/HkpM8l1Up.png) 打Tennis.score().ae,一樣藉由除錯意見去Tennis class裡面建立score方法。 ![螢幕擷取畫面 2023-12-07 154303](https://hackmd.io/_uploads/rkfsDgy8a.png) 跑一下測試,看看是否符合預期。 ![螢幕擷取畫面 2023-12-07 154436](https://hackmd.io/_uploads/r1Ya_lJUa.png) ``` 這邊提供一個手殘的案例: 因為用Assertions EqualTo這個方法, 去比對tennis裡面的score方法回傳的結果,是不是我們預期的字串。 那我這邊的報錯就是在說,我們在單元測試預期的是Love All。 但實際上score方法回傳的是Love all。 這也是TDD的精神所在: 測試情境->出錯->測過、得到預期->重構、優化 ``` ## 抽出方法 ### 使new Tennis()成為field 再測過以後,我們就來進行現階段的重構、優化。 為了之後每一個測試方法不用再new一個Tennis物件,我們把new Tennis()抽出去成為這個單元測試的實體變數。 ![螢幕擷取畫面 2023-12-07 161122](https://hackmd.io/_uploads/SJbOAxyL6.png) ![螢幕擷取畫面 2023-12-07 161314](https://hackmd.io/_uploads/Hk_C0xkIp.png) ### 抽出Assertions 基於單元測試裡面的東西,不應該被套件干擾,應該要分離的想法。 所以把Assertions那一行全選,使用command + alt + M 去把該套件拉出來變成一個獨立的套件方法。 ![螢幕擷取畫面 2023-12-07 161540](https://hackmd.io/_uploads/By8HyZ1LT.png) ### 把寫死的值變成變數 那既然我們都已經抽成一個獨立的方法了,當然就會希望驗證的分數情況不是寫死的。 那我們透過 Introduce parameter去把套件裡的原本寫死的字串取代改成可變動的變數。 這樣只要呼叫該方法,並給予驗證的字串即可,不用每次都再創一次驗證方法。 ![image](https://hackmd.io/_uploads/H1WzXZJ8T.png) ![image](https://hackmd.io/_uploads/BkmS7byI6.png) ## 得一分的情境 ### 建立得一分情境的測試方式 這裡我們會去加一個第一位選手得一分的方法:tennis.firstPlayerScore(); 一樣也是透過在 Test 裡面呼叫不存在的方法,所報的錯誤去使用除錯建議,去 Tennis 這個 class 中創建第一位玩家得分的方法。 ![image](https://hackmd.io/_uploads/ryNeSbkI6.png) 跑測試會發現出錯,是因為得分方法裡會回傳 “love_all”,而且得一分的方法裡面什麼都沒有。 會使得我們期待的(fifteen_love)與實際的(love_all)不相符。 ![image](https://hackmd.io/_uploads/BJWvP-kLa.png) ### Tennis類別添加得分情境 所以,回到 Tennis 類別,去把得一分的方法裡加上一個累加的變數(firstPlayer++),也靠著除錯建議去外面宣告一個firstPlayer的field。 ![image](https://hackmd.io/_uploads/H15gd-kU6.png) 接著去score裡面,去添加得一分的情況:firstPlayer==1.if。 然後在這個情況中去return “fifteen love”。(蝦趴的做法:先打字串.return) ![image](https://hackmd.io/_uploads/SyFI_-1L6.png) 完成後跑一下測試。 ![image](https://hackmd.io/_uploads/rJaPObyIp.png) ## 得兩分的情境 ### 建立得兩分情境的測試方式 一樣也是從 Test 這邊開始,先去寫一個thirty_love的測試方法。 比起前面得一分的方法,一樣是多了一次得分的方法呼叫的行為。 那這裡直接把打字游標移去得分方法那一行, 然後按快捷ctrl + D,把該行複製到下一行。 ![image](https://hackmd.io/_uploads/ryDIKZ18T.png) 然後跑一下測試,一樣發現因為還沒有把得兩分的情境完善,所以回傳的預期與實際的不同。 ![image](https://hackmd.io/_uploads/S1CqYWk86.png) ### Tennis類別添加得分情境 一樣先回到Tennis的類別,把得一分的情境全選起來複製到下一段(一樣可以全選ctrl + D)。 改成得兩分的情境,並且跑個測試 ![image](https://hackmd.io/_uploads/SkdE9W18a.png) ``` 題外話,再改寫程式的時候,盡量保有原來程式的結構。 比如說,下圖,原本是沒有紅色圈起來那段的,那我們再加入這個情境的時候, 應該讓下面的程式碼保有原本的架構, 而不是把紅圈的那段插入在firstPlayer==1的情境跟return之間,破壞了原本的架構邏輯。 要盡量以最小的改變方式去完成現在的目的,之後再重構的時候,再去做這件事情(破壞原本架構)。 詳情的話,請參考TDD(Test-Driven Development: By Example)(TDD) ``` ![image](https://hackmd.io/_uploads/H12aqZ1La.png) ## 添加查表優化程式 接著我們要去做一個重構,讓這支程式可以去做查表(把情境與相對的行為結果放到表裡面去查詢)。 ### 宣告查表 所以我們去判斷條件的上方,插入宣告要查的表 : new HashMap<Integer, String>(){{裡面put情境與行為}}.var (打情境重複的可以善用ctrl + D) ![image](https://hackmd.io/_uploads/HkEw1zJLp.png) 然後把情境裡面的return 寫死的字串改成從表裡查出來。 改完跑測試看有沒有測過。 ![image](https://hackmd.io/_uploads/r1PAJfk8T.png) ### 把查表方式抽出、並優化寫法 跑過測試後,也一樣把宣告的查表拉出計分的方法,讓他成為field。 ![image](https://hackmd.io/_uploads/H1lPlf1La.png) 順便把剩下的情境中查表裡寫死的字串改成變數firstPlayerScore。 ![image](https://hackmd.io/_uploads/B1fTeGkIp.png) ## 合併並簡化類似的情境 ### 合併判斷條件 然後再把打字游標移到 if 前面,按 alt + enter,選擇 merge sequential 'if' statements。 ![image](https://hackmd.io/_uploads/rk9m-zyIp.png) 合併之後,考慮到之後會還有更多的得分情況, 所以把等於一或等於二的敘述條件刪掉,改成大於零。 ![image](https://hackmd.io/_uploads/HJ1DWzJIT.png) ### 重構方法 測試跑過後,回到TennisTest的類別,那因為每得分一次,都會呼叫一次firstPlayerScore的方法。那像得兩分就會呼叫兩次,有點麻煩。 所以我們去得兩分的地方重構一下,先把兩個重複的firstPlayerScore方法選起來, 然後按快捷 ctrl + alt + M 把它抽出來。 ![image](https://hackmd.io/_uploads/HkULnyeUp.png) 那我們把名字取叫givenFirstPlayerScore,然後在裡面新增一行打 fori + enter 創一個for迴圈,可以依照次數去執行tennis.firstPlayerScore(); ![image](https://hackmd.io/_uploads/SJ0OT1xLp.png) 那一樣在確認方法可以跑過測試後,考慮到為了更加方便,我們把原先寫死的迴圈上限2,透過Introduce parameter(快捷:alt + ctrl + P)把它改寫成變數。 ![image](https://hackmd.io/_uploads/rkkkCyxLa.png) 順便把fifteen_love測試方法裡面原先呼叫tennis類別的firstPlayerScore()方法,改成呼叫測試類別中的givenFirstPlayerScore(),並帶入參數1。 ![image](https://hackmd.io/_uploads/ByLFRke86.png) 這樣就完成了第一位球員0:0、0:1、0:2三種情況的方法跟測試方法。 ## 得三分的情境 得三分的情境跟第二位球員得一到三分的情境, 這邊的步驟跟前面的步驟以及邏輯一模一樣,就快速帶過。 創測試情境並測試,發現是結果不符預期。 ![image](https://hackmd.io/_uploads/B1WFVxgLa.png) 修正錯誤:查表增加情境元素,並跑測試。 ![image](https://hackmd.io/_uploads/By22HegIa.png) ## 第二位球員得一分情境 創建測試情境:第二位球員計一分,並且回傳Love-Fifteen的比數。 ![image](https://hackmd.io/_uploads/S18OUleLT.png) 利用除錯提示,創建第二位球員計分方法,並跑測試看結果。 ![image](https://hackmd.io/_uploads/rJA1FleLT.png) 在score()裡面建立第二位球員的顯示比數邏輯givenSecondPlayerScore(),並跑測試看結果。 ![image](https://hackmd.io/_uploads/r1ZFcgeI6.png) ## 第二位球員得兩分情境 創建測試情境:第二位球員計兩分,並且回傳Love-Thirty的比數。 ![image](https://hackmd.io/_uploads/ByIKjgeUT.png) 在第二位球員的顯示比數邏輯中加入兩分的情境,並跑測試看結果。 ![image](https://hackmd.io/_uploads/H1rM2gx8T.png) ### 進行優化 改為查表,並跑測試觀測功能是否正常。 ![image](https://hackmd.io/_uploads/BkEfy-xL6.png) 合併敘述,並跑測試觀測功能是否正常。 ![image](https://hackmd.io/_uploads/ByEV1ZxLT.png) 在測試將計分方式統整成方便重複利用的元件,並跑測試觀測功能是否正常。 ![image](https://hackmd.io/_uploads/SyVJTgg8a.png) 把原先直接呼叫計分方式的地方用元件替換掉,並跑測試觀測是否正常。 ![image](https://hackmd.io/_uploads/Byax0ggIp.png) ## 第二位球員得第三分 會發現只要很歡樂的加上測試情境,就會跑過。 因為我們前面經過優化已經完整了只有第二位球員得分的幾乎所有情況。 ![image](https://hackmd.io/_uploads/ByzuxbgU6.png) ## 平手情境 考慮完個別得分的情境後,接著來著手平手的部分。 ### Fifteen All 也一樣先從測試情境著手。 ![image](https://hackmd.io/_uploads/H1khzZg8T.png) 回到Tennis類別,會發現我們除了Love All以外,沒有其他平手的可能 ![image](https://hackmd.io/_uploads/B1na4WxIT.png) 那這邊我們可以把零分Love這個情境也考慮改成查表的方式。 所以在表中加入元素,並且把return中寫死的Love字串改為查表的方式。 ![image](https://hackmd.io/_uploads/BJfwH-lU6.png)| 這時候會發現跑出來的結果不如預期,我們再把敘述改變一下。 ![image](https://hackmd.io/_uploads/SklQwbxUp.png) 我們把敘述從兩位球員個別計分,改成兩位球員同分跟不同分的情形。 利用前面在"合併判斷條件"講的,把打字游標移到 if 前面,按 alt + enter,選擇 merge sequential ‘if’ statements。 把兩位球員的判斷合併在一起後,把條件敘述改成兩者不相等。 最後再加入十五分平手的條件即可。 ![image](https://hackmd.io/_uploads/rkcN5WlUa.png) #### 優化 因為上面是寫死的,首先把它改為查表的方式。 ![image](https://hackmd.io/_uploads/SJd9obe8a.png) 那會發現最後的 return "Love All",Love可以查表查到,所以判斷條件會有點多餘,只要留有查表的那個,其他刪掉即可。 ![image](https://hackmd.io/_uploads/Hki_T-eUa.png) 不用滑鼠的做法是把 return "Love All"改成 if 判斷條件內的敘述。 然後把光標移到判斷句前的if 按 alt + enter,選Collapse 'if' statement。 ![image](https://hackmd.io/_uploads/B1rTh-gIT.png) ### Thirty All 因為我們上面邏輯已經完善,會發現一次就跑過了 ![image](https://hackmd.io/_uploads/rkLBEGgUp.png) #### 優化 為了方便閱讀跟低耦合度,把score()中能抽成方法的地方抽出來。 ![image](https://hackmd.io/_uploads/rySbSGg8T.png) ### Deuce 一樣先在測試寫測試情境 ![image](https://hackmd.io/_uploads/BkqipmeIa.png) 再回到Tennis寫邏輯,並跑測試觀測是否正常。 ![image](https://hackmd.io/_uploads/HkmUCQxL6.png) ## 第一位球員領先 回到單元測試寫測試情境,那因為接下來都會考慮到Deuce的情況,所以我們先去把兩位選手計分同分的方法拉出來,成為獨立的方法,以方便使用。 那這個時候ide會很好心地問你,要不要把上面的平手情境都取代掉。 但因為我們只考慮Deuce的情況,所以就選Keep Original Signature就可以了。 ![image](https://hackmd.io/_uploads/Sy0nx4eUp.png) 那我們一樣些寫測試情境,英文名字就自己隨便選。 ![image](https://hackmd.io/_uploads/BJ6WfElIp.png) 很顯然,比數不同的條件裡面,只有查表,而表裡面又沒有大於3的東西,所以會跑出null。 所以我們就把Advence歸類到比數不同的區塊裡面。 那一樣先只考慮當前測試的情境就好,所以只先考慮一位球員。 ![image](https://hackmd.io/_uploads/H19DEElI6.png) ### 優化 那考慮到我們是要設計一個計分器,球員名稱不能寫死。 所以我們去建立一個建構子,讓使用者可以自由地給予姓名去當作球員名稱的初始值。 那用快捷鍵的方式是先把姓名跟Adv分開,把打字游標移到姓名處,用Introduce field的快捷鍵,然後initialize in選建構子。 ![image](https://hackmd.io/_uploads/HkMc8NxU6.png) 因為我們期待的是名字是我們給的,所以去把姓名抽成參數。 ![image](https://hackmd.io/_uploads/HJR6D4g8p.png) 改好後,跑一下測試觀測功能是否正常。 ![image](https://hackmd.io/_uploads/ryWYONl8p.png) ## 第二位球員領先 一樣先建立測試情境去跑 ![image](https://hackmd.io/_uploads/HyyXtVeIp.png) 來到Tennis類別,做三件事。 1.改第一個if條件為只要任意球員大於三分。 2.改第二個分差一分的條件為絕對值。小撇步:在secondPlayerScore後面加.abs 3.return前去判斷誰領先。小撇步:打完三元運算子的判斷後.var 跑測試 ![image](https://hackmd.io/_uploads/rk6VeSlLT.png) ### 優化 一樣去重複上面的作法,把第二位球員的名字寫進建構子裡面。 ![image](https://hackmd.io/_uploads/Sy70xBgLT.png) 跑過測試後,為了方便閱讀跟低耦合度,開始把方法抽出來。 ![image](https://hackmd.io/_uploads/r1-NNBeIa.png) 這裡有個小撇步 : 把打字游標移到宣告變數的位子,按ctrl + alt + n (inline variable) (inline variable 內嵌變數 : 通常是指在程式碼中直接使用或嵌入變數的值,而不需要額外的變數聲明。) ![image](https://hackmd.io/_uploads/r16SVSl8T.png) ![image](https://hackmd.io/_uploads/HJ0oNSgUp.png) ## 優勝 一樣先寫測試,那因為是在超越三分的情況下去判斷分差是否是一分。 所以只要不是,就是贏球了。 所以可以寫成 ![image](https://hackmd.io/_uploads/rkNZiBeUa.png) 其實到這邊就結束了,可以再依自我喜好去整理更乾淨,把功能切割得更清晰。