---
# System prepended metadata

title: H03

---

# 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)
