# 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. 輸入層

針對特定情況,進行例外的處理
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 去做紀錄 + 輸出

### 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已經有選擇過了喔"));
}
```
測試的結果

### 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) 種 (本壘不會列入考量)
再加上一筆針對輸入資料的錯誤測試
測試的結果

### 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()));
}
```
測試的結果

### Service 最終結果


# H03B hw01+JUnit
## 本次的目標
將 hw01 舊有程式碼進行重構並加上 JUnit
之前我們是透過一些寫好的測試資料去測試結果
那這次透過 JUnit 去測試中間的過程是否有問題
### 使用此專案
Configuarations 上有
RunMain -> 執行主程式
SortingRankingTest -> 執行測試
### 1. 針對 SortingRanking 內的 rankingLeague 測試
- 為何針對測試?
因為這個方法是最主要負責排序出各隊伍的順序
所以就算出現資料讀取沒問題,但排序卻發生錯誤
會造成產出一個錯誤的結果
- 遇上的問題:
private 不能直接被呼叫出來做測試
有鑑於此,我把這個 function 改為 default
稍微看了別人的作法,結論是建議如果經常要去做測試的話
不要設定為 private,因為用反射方法會造成
- 程式碼較為複雜,需要額外處理反射相關的異常(InvocationTargetException, IllegalAccessException)
- 可能影響測試的可讀性和維護性
- 執行效率較低,因為反射會繞過 Java 的訪問控制檢查

- 測試內容
- 排序結果是否符合預期(由大到小)
- 是否有針對組別不夠進行拋出例外以及例外的 message 是否吻合

- 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
}
]
}
}
```

### 3. 針對 ShowMLBResult 的測試
測試是否傳入路徑可以正常輸出
傳入錯誤路徑會不會拋出對應的例外

### 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()的處理

### 5. 實作 List 與 抽象 List

舊有的程式碼,因為知識不足,會習慣將回傳值硬性設定為 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 最終結果


## 參考:
[如何做到 System.out 的測試](https://www.baeldung.com/java-testing-system-out-println)
## 心得
測試程式碼,個人覺得重複性很高,更多的是設計測試資料有哪些
這作業寫好久,真的很久很久 OMG
應該說猶豫很久,心想一定要做到 100% 嗎
用了 AI 產生一堆自己看不懂的程式碼,這樣好嗎
不排除有用 AI 在這項作業,但還是盡力以自己完成
不然這樣都沒甚麼進步
也感覺自己寫了很多很雜亂的 Code
這如果是一個大專案,日後一定會出現很多延伸問題哈哈哈
也感覺大多是為了測試而測試
雖然會想唉呦好多好煩
但看到 100% 我好棒XD
