# H03 [筆記網址](https://hackmd.io/@ruserxd/H1HAF5aM1g) [H03_A](#H03A-封殺)| [H03_B](#H03B-hw01+JUnit) 作者: D1149580_資訊三乙_吳堃瑋 # H03A 封殺 ## 本次目標 這次的目標是製作一個能夠判斷目前可封殺的壘包有哪些 並透過 JUnit 與 Jacoco 進行測試 以下的程式碼都會透過數字去代表壘包 0 -> 本壘, 1 -> 一壘, 2 -> 二壘, 3 -> 三壘 ### 使用此專案 Configuarations 上有 RunMain -> 執行主程式 All_In_Service -> 執行所有測試 ## 程式設計 分為 3 個 Service ScannerBaseStatus -> CountBaseBallForceOut- > OutPutForceOutResult 輸入層 -> 處理層 -> 輸出層 ### 1. 輸入層 ![截圖 2024-11-23 凌晨12.09.29](https://hackmd.io/_uploads/Bk26nX0zkx.png =50%x) 針對特定情況,進行例外的處理 1. 輸入應該介於 0 ~ 4 2. 重複輸入的部分不做處理 ```java= // 針對已經選擇過的,不重複 if (base >= 0 && base < 3 && baseStatus[base]) { System.out.println("這項" + base + "已經有選擇過了喔"); continue; } // 省略.. // 針對已經選擇過的,不重複 default: // 針對輸入的範圍 System.out.println("請輸入在範圍 (0 ~ 4) 內"); break; } ``` 最後的結果會透過 log 去做紀錄 + 輸出 ![截圖 2024-11-22 晚上11.45.27](https://hackmd.io/_uploads/rJ8HD7RM1x.png) ### 2. 處理層 boolean[] base 代表是否可封殺的壘包 1壘預設為 TRUE,因為只要有打擊出去的情況都可以封殺一壘 接著需要去判斷哪些壘包有人,藉此去判斷出可封殺壘包 一壘有人,那二壘可以封殺 一、二壘有人,那三壘可以封殺 一、二、三壘有人,那本壘可以封殺 ```java= base[1] = TRUE; if (baseBagStatus[1] == TRUE) { base[2] = TRUE; if (baseBagStatus[2] == TRUE) { base[3] = TRUE; if (baseBagStatus[3] == TRUE) { base[0] = TRUE; } } } ``` 此外,我們需要確保輸入層傳入的資料內容要保持在 4 個 只需要確保輸入層撰寫的程式碼傳過來的資料是符合大小,所以不用每次都確定 ```java assert baseBagStatus.length == 4:"壘包的狀態必須為 4 才行!!!"; ``` ### 3. 輸出層 簡單的輸出 輸入的內容 -> 可封殺的壘包 e.x. ``` 1B 2B 3B -> HB 1B 2B 3B 2B -> 1B 1B 2B -> 1B 2B 3B X -> 1B ``` ## JUnit 測試 ### 1. 輸入層的測試 因為我們輸入,有時候會System.out 一些資訊出來,所以要去測試是否有正確丟出這些訊息 模擬輸入 -> 將輸出資訊存起來 -> 測試是否符合 ```java= // 模擬輸入用的 private ScannerBaseStatus scannerBaseStatus; // 負責儲存輸出 private final ByteArrayOutputStream outputStreamCaptor = new ByteArrayOutputStream(); @Test @DisplayName("測試重複輸入的情況") void testDuplicateInput() { // 模擬輸入 String input = "1\n1\n2\n4\n"; InputStream in = new ByteArrayInputStream(input.getBytes()); System.setIn(in); scannerBaseStatus.scanner = new java.util.Scanner(System.in); boolean[] result = scannerBaseStatus.getBaseStatus(); // 省略... // 查看輸出的內容是否有符合 assertTrue(outputStreamCaptor.toString().contains("這項1已經有選擇過了喔")); } ``` 測試的結果 ![image](https://hackmd.io/_uploads/S1njL9JQ1g.png) ### 2. 處理層的測試 兩個陣列的運用 ```java= // 用來存放預計輸入的資料 boolean[] testBaseStatus = new boolean[4]; // 用來存放預計結果的資料 boolean[] expectForceOutStatus = new boolean[4]; ``` 有鑒於會經常使用到這兩個陣列,設計一個 BeforeEach 做每次的初始化 ```java= @BeforeEach void setTestBaseStatus() { log.info("對測試資料先進行初始化"); for (int i = 0; i < 4; i++) { testBaseStatus[i] = FALSE; expectForceOutStatus[i] = FALSE; } } ``` 因為我們壘包會有人的情況為 8(2^3) 種 (本壘不會列入考量) 再加上一筆針對輸入資料的錯誤測試 測試的結果 ![截圖 2024-11-23 凌晨12.27.48](https://hackmd.io/_uploads/BkiMWV0fke.png =70%x) ### 3. 輸出層的測試 這邊遇上可能是換行與空白的差異問題 所以把所有的空白跟換行都移除 ```java= // 移除所有空白和換行 // \\s 代表可多種 \t \n \r space... private String removeAllWhitespace(String str) { return str.replaceAll("\\s+", ""); } ``` 然後再去做比對相同與否 ```java= @Test @DisplayName("測試沒有人在壘包的情況") void testNoBaseRunner() { log.info("測試沒有人在壘包的情況"); boolean[] baseStatus = {false, false, false, false}; boolean[] canForceOutBase = {false, false, false, false}; outPutForceOutResult.outForceOutResult(baseStatus, canForceOutBase); assertEquals("結果X->", removeAllWhitespace(outputStreamCaptor.toString())); } ``` 測試的結果 ![image](https://hackmd.io/_uploads/SygJhc1m1l.png) ### Service 最終結果 ![image](https://hackmd.io/_uploads/r1zpYqyQ1x.png) ![image](https://hackmd.io/_uploads/HyMGz31XJx.png) # H03B hw01+JUnit ## 本次的目標 將 hw01 舊有程式碼進行重構並加上 JUnit 之前我們是透過一些寫好的測試資料去測試結果 那這次透過 JUnit 去測試中間的過程是否有問題 ### 使用此專案 Configuarations 上有 RunMain -> 執行主程式 SortingRankingTest -> 執行測試 ### 1. 針對 SortingRanking 內的 rankingLeague 測試 - 為何針對測試? 因為這個方法是最主要負責排序出各隊伍的順序 所以就算出現資料讀取沒問題,但排序卻發生錯誤 會造成產出一個錯誤的結果 - 遇上的問題: private 不能直接被呼叫出來做測試 有鑑於此,我把這個 function 改為 default 稍微看了別人的作法,結論是建議如果經常要去做測試的話 不要設定為 private,因為用反射方法會造成 - 程式碼較為複雜,需要額外處理反射相關的異常(InvocationTargetException, IllegalAccessException) - 可能影響測試的可讀性和維護性 - 執行效率較低,因為反射會繞過 Java 的訪問控制檢查 ![image](https://hackmd.io/_uploads/S1ZIKGJmkg.png) - 測試內容 - 排序結果是否符合預期(由大到小) - 是否有針對組別不夠進行拋出例外以及例外的 message 是否吻合 ![image](https://hackmd.io/_uploads/SkNfiL1Qyg.png) - Before Each 的運用 因為每次都會用到兩個變數,避免出現問題 (a, b 同時對一個資料進行插入,b 誤使用到 a 的資料) 因此每次都對這兩個變數進行初始化 ```java= private SortingRanking sortingRanking; private HashMap<String, List<TeamData>> testData; @BeforeEach void initial() { sortingRanking = new SortingRanking(); testData = new HashMap<>(); } ``` ### 2. 針對 GetDataFromJSON 的 ParseException 測試 這個蠻有趣的,我本來是鍵盤亂敲在一個 json 檔案 但發現原來不是這樣的概念 而是 Json 的語法沒有符合 e.x. ```json= { "American League": { // 有基本的 JSON 結構 "East": [ { "team": Baltimore", // 引號不對稱 "wins": 101, "losses": 61 } ] } } ``` ![image](https://hackmd.io/_uploads/H13KDkWXye.png) ### 3. 針對 ShowMLBResult 的測試 測試是否傳入路徑可以正常輸出 傳入錯誤路徑會不會拋出對應的例外 ![image](https://hackmd.io/_uploads/SyDwuyZQkx.png) ### 4. 針對 ProcessJson 的測試 這個也蠻有趣的 因為原本的程式碼是用 if .... else if ... 然後就沒了 那這就會造成測試上不能 100% 的問題 為何?? 因為沒有處理 else ```java= private final Map<String, HashMap<String, List<TeamData>>> leagueToHashMap = Map.of( leagues[0], hashAmericanLeague, leagues[1], hashNationalLeague ); ``` 所以就改成 map 去用 Key get()的處理 ![image](https://hackmd.io/_uploads/ryINY1bQye.png) ### 5. 實作 List 與 抽象 List ![image](https://hackmd.io/_uploads/S1oZkLy7Jg.png =80%x) 舊有的程式碼,因為知識不足,會習慣將回傳值硬性設定為 ArrayList ```java public HashMap<String, ArrayList<TeamData>> processJson(String filePath) { ``` 但這樣會減少程式碼的多態性、可更動性 ```java public HashMap<String, List<TeamData>> processJson(String filePath) { ``` 改成這樣日後可以使用ArrayList、LinkedList和Vector當作我們的 Value 同時在測試,我們要傳入資料時,也不會因為測試員寫 LinkedList 造成報 出錯誤的問題 e.x. 正常情況都會使用 ArrayList 如果突然需要實作經常需要刪除插入的狀況,那就會使用到 LinkedList 在回傳值都是回傳 List 時,我們只需要去考慮到,一開始建立出的變數設計就好 我的習慣是這樣,在實作時只要修改 initial 方法裡面的 new 就好 ```java= // 初始化 HashMap private static HashMap<String, List<TeamData>> initialMap() { HashMap<String, List<TeamData>> hashLeague = new HashMap<>(); for (String region : regions) hashLeague.put(region, new ArrayList<>()); return hashLeague; } ``` 認為這樣是更好的設計 補充: 好奇心之下查了,那 AbstractList 跟 List 又有甚麼區別 前者會先實作好一些功能,後者相較於前者更像是有個架構而已 所以如果是要達到多態的功能會建議用後者 前者則是如果要自訂一個 List 會用 ### Service 最終結果 ![image](https://hackmd.io/_uploads/SJtdHy-Qye.png) ![image](https://hackmd.io/_uploads/BkT2BkZmJl.png) ## 參考: [如何做到 System.out 的測試](https://www.baeldung.com/java-testing-system-out-println) ## 心得 測試程式碼,個人覺得重複性很高,更多的是設計測試資料有哪些 這作業寫好久,真的很久很久 OMG 應該說猶豫很久,心想一定要做到 100% 嗎 用了 AI 產生一堆自己看不懂的程式碼,這樣好嗎 不排除有用 AI 在這項作業,但還是盡力以自己完成 不然這樣都沒甚麼進步 也感覺自己寫了很多很雜亂的 Code 這如果是一個大專案,日後一定會出現很多延伸問題哈哈哈 也感覺大多是為了測試而測試 雖然會想唉呦好多好煩 但看到 100% 我好棒XD ![image](https://hackmd.io/_uploads/r17cKyb71x.png)