# 📖 Testing
## Toolkits
+ 🔗 [**Python : Pytest**](https://hackmd.io/@RogelioKG/pytest)
+ 🔗 [**Java : JUnit**](https://hackmd.io/@RogelioKG/junit)
+ 🔗 [**Vue : Vitest**](https://hackmd.io/@RogelioKG/vitest)
## Terms
### system requirement specification (SRS)
軟體需求規格書。
### system design document (SDD)
系統設計書。
### application under test (AUT)
將要被測試的應用 (包含多個系統)。
### system under test (SUT)
將要被測試的系統。
### class under test (CUT)
將要被測試的類別。
### method under test (MUT)
將要被測試的方法。
### test input
將提供給 AUT / SUT 的測試資料。
### expected output
程式或系統預期的執行結果。
### test data
[test input](#test-input) 與其對應的 [expected output](#expected-output)。
### test case
測試案例。\
它應當描述<mark>當系統在一個特定條件 (condition) 下時預期的反應</mark>。
通常我們可以根據測試案例的條件,來決定 [test data](#test-data) 的具體值。
+ 例如
根據一個登入功能,可以設計一個測試案例:\
當使用者輸入正確的帳號和密碼時,應該成功登入。
### negative test case
一種反向的測試案例。
它通常會給定 <mark>bad [test input](#test-input)</mark>,看看 AUT / SUT 有何反應。
也就是說,這種測試案例是在<mark>考驗系統的例外處理做得好不好</mark>。
+ 補充
這個環節通常可以觀察出你的<mark>社會化程度</mark>,\
因為<mark>人心是險惡的</mark>,你永遠不知道使用者用什麼神祕姿勢在敲鍵盤 (bad input 千奇百怪);
因為<mark>公司的軟體服務是堅若磐石的</mark>,面對邪惡 bad input 絕不停機屈服。
+ 記住
在多人團隊裡,<mark>不做例外處理的那個 developer 簡直就是老鼠屎</mark>。
### test suite
[Test Case](#Test-Case) 的集合。
### true oracle
<mark>已存在一個軟體,能根據指定輸入,輸出絕對正確的結果</mark>。
這個軟體就被稱作 true oracle (<mark>在現實情境中,完美的幾乎不存在</mark>)。
有了他,你的 test case generation 就很方便。
實例:一個運作多年、人人稱羨的成熟系統,通常就逼近為一個 true oracle。
(雖然它可能是個舊系統,因效能問題要進行 refactor。但是我們在開發新系統時,還是可以拿它做驗證)
### partial oracle
<mark>已存在一個特定算法,能根據輸入和輸出,來告訴你輸出是否正確</mark>。
這個特定算法就被稱作 partial oracle (<mark>僅在簡單場景下存在</mark>)。
有了他,你的 test case generation 就很方便。
實例:比如說檢驗排序算法的正確性,只要檢查左邊的數字是否必定大於右邊,就好了。
### testing left-shift
研究指出:
+ 引入 BUG 最常發生在:<mark>開發階段</mark> (Coding)。若此時修正,成本低。
+ 發現 BUG 最常發生在:<mark>系統測試階段</mark> (System Test)。若此時修正,成本高。
而進行測試可以發現 BUG,\
因此為了降低修正成本,我們需要將<mark>測試左移</mark> (盡可能早地完成測試)。
+ 測試左移前

+ 測試左移後

### incoming dependency
給定輸入的資料,通常是經由先前的操作而被動流入 unit of work 的資料。\
例如:query 資料庫的資料、API 的回應。
### outgoing dependency
呼叫外部函式,這是 unit of work 的一種 exit point。\
例如:寫入資料庫、呼叫 API 或 webhook。
### stub | mock | spy
|類型|定義|目的|範例|
|--- |--- |--- |--- |
|**stub**|假的輸入,模擬實作並<mark>取代</mark>原本的實作|隔離 incoming dependency|假造的測試資料、物件或函式|
|**mock**|假的輸出,模擬實作並<mark>取代</mark>原本的實作|隔離 outgoing dependency|呼叫假的服務、寫入假的資料庫|
|**spy**|監控呼叫,模擬實作但<mark>不取代</mark>原本的實作|記錄互動|是否被正確呼叫|
```mermaid
flowchart LR
ut["CUT / SUT"]
stub[["stub"]]
ut <--->|communicate| stub
ut <--->|assert| test
```
```mermaid
flowchart LR
ut["CUT / SUT"]
spy[["spy"]]
spy <--->|communicate| ut
spy <--->|assert| test
```
## Rules
### F.I.R.S.T 原則
F.I.R.S.T Principles of Unit Testing
1. `Fast`:<mark>快速的</mark>
2. `Independent`:<mark>獨立的</mark> -- 測試之間要相互獨立,如果互相依賴的話,一個測試失敗會影響其他測試也都失敗
3. `Repeatable`:<mark>可重複的</mark> -- 要在任何環境都可重複執行
4. `Self-Validating`:<mark>可自驗證的</mark> -- 可從 report 直接了解失敗原因
5. `Timely`:<mark>及時的</mark> -- 最好是在寫程式之前先寫測試 (TDD 概念)
### 3A rule
考量 Independent 時遵守該原則
1. `Arrange` (Given)\
建立此測試案例需要的初始值,和思考好的命名和變數命稱來讓測試更容易理解
2. `Act` (When)\
呼叫目標方法
3. `Assert` (Then)\
驗證是否符合預期
## Management

### Tester
+ 根據 test spec、test plan,編寫 <mark>test case</mark>
+ <mark>事後</mark>發現缺陷
+ 品質是<mark>檢驗</mark>出來的
+ 測試只是幫助我們了解品質的現況
+ 專注於測試的具體實現
+ 近年來 programmer 也需扮演 tester 的角色
### QA
+ 編寫 <mark>test spec</mark>、<mark>test plan</mark> 或 <mark>automation</mark>
+ <mark>事前</mark>預防缺陷
+ 品質是<mark>計畫、設計、建置</mark>出來的
+ 思考什麼樣的品質能達到公司的目的
+ 專注於產品需求的全貌
+ 將<mark>品質的觀念</mark>帶給 developer team
+ 不能歸 developer team 管轄,如同司法部門不能在行政部門底下
### SDET
+ Software Development Engineer in Test
+ 建立<mark>測試架構平台</mark>
+ 設計與優化<mark>自動化測試架構</mark>
### QA Manager
+ 策略、規劃、部門以及人員發展
## V model

## Test-Driven Development (TDD)
### Reference
可參考:[🎬 ArjanCodes - Test-Driven Development In Python](https://youtu.be/B1j6k2j2eJg)
### 核心理念
<mark>先寫測試,再做開發</mark>
### 優點
+ 可確保每次改動的品質,不會改 A 壞 B
+ 可利用 test case 釐清使用情境,減少溝通成本
+ 新人可透過 test case 更了解每個 function 在做什麼事,包含呼叫情境、傳入引數及預期結果
### 開發三步驟
+ 第一步 <span style="color: red;">**紅燈 RED**</span>
> 寫測試,並且執行測試 (此時 test case 應當全 FAILED。很正常,因為你還沒實作介面)
+ 第二步 <span style="color: green;">**綠燈 GREEN**</span>
> 寫程式。目的是要讓所有 test case PASSED。
+ 第三步 <span style="color: grey;">**重構 REFACTOR**</span>
> 重構程式。增加程式碼的可讀性、記憶體的優化、運算效能的優化...。與此同時須保持所有 test case PASSED。

## Test Plan


## Testing Requirements
| | SRS | SDD | code |
| --- | --- | --- | --- |
| System Test | ✓ | | |
| Integration Test | ✓ | ✓ | |
| Unit Test | ✓ | ✓ | ✓ |
## Test Case Convention
### naming
+ `MethodUnderTest_TestedState_ExpectedBehavior`
+ 方法:`public double calculateDiscount(double price, double discount)`
+ 測試:
+ `calculateDiscount_ValidPriceAndDiscount_CorrectDiscountApplied`\
檢查當傳入有效的價格和折扣百分比時,是否正確計算出折扣後的價格
+ `calculateDiscount_ZeroDiscount_NoDiscountApplied`\
當折扣為零時,返回的價格應該等於原價格
+ `calculateDiscount_NegativeDiscount_ThrowsError`\
如果傳入負數折扣,應該拋出錯誤或返回一個適當的錯誤訊息
### comments
+ `given`:給定初始條件
+ `when`:在某個狀態下
+ `then`:預期產生什麼結果
```java
public class DiscountCalculator {
public double calculateDiscount(double price, double discount) {
if (discount < 0 || discount > 100) {
throw new IllegalArgumentException("Discount must be between 0 and 100.");
}
return price - (price * discount / 100);
}
}
```
```java
import org.junit.Test;
import static org.junit.Assert.*;
public class DiscountCalculatorTest {
@Test
public void testCalculateDiscount_ValidDiscount_ReturnsCorrectPrice() {
// given: price is 200 and discount is 20
DiscountCalculator calculator = new DiscountCalculator();
double price = 200;
double discount = 20;
// when: calculateDiscount method is called
double result = calculator.calculateDiscount(price, discount);
// then: the result should be 160
assertEquals(160.0, result, 0.001);
}
@Test(expected = IllegalArgumentException.class)
public void testCalculateDiscount_NegativeDiscount_ThrowsException() {
// given: price is 200 and discount is -10
DiscountCalculator calculator = new DiscountCalculator();
double price = 200;
double discount = -10;
// when: calculateDiscount method is called with invalid discount
// then: IllegalArgumentException should be thrown
calculator.calculateDiscount(price, discount);
}
@Test
public void testCalculateDiscount_ZeroDiscount_ReturnsOriginalPrice() {
// given: price is 150 and discount is 0
DiscountCalculator calculator = new DiscountCalculator();
double price = 150;
double discount = 0;
// when: calculateDiscount method is called
double result = calculator.calculateDiscount(price, discount);
// then: the result should be the same as the original price, i.e., 150
assertEquals(150.0, result, 0.001);
}
}
```
# 功能性測試 Functional Testing
## 單元測試 Unit Testing
### 黑箱測試 black-box testing
在<mark>不知道程式內容</mark>的情況下所進行的測試 (通常需要 [SDD](#System-Design-Document-SDD))
+ **等價區間 equivalence partitioning**
+ 同等價區間內的 test input,對於驗證程式的正確性,擁有相同的價值
範例:假設需要輸入 username 向系統註冊帳號,username 必須長度在 3~6 之間,此時即出現 3 個等價區間,一個是小於 3,一個介於 3~6,一個是大於 6。\
註:這也是<mark>不需把所有 test input 排列組合都丟進來測試的本質原因</mark>。
+ 每個等價區間需對應一個 test case
+ **邊界值分析 boundary value analysis**
+ 比起在特定範圍中央,<mark>錯誤更容易在輸出入資料的邊界發生</mark>
+ 除了選擇在等價區塊中的邊界值,也需要測試區塊外的邊界值
+ 等價區間只以輸入建立測資,邊界值分析同時使用輸入和輸出


### 白箱測試 white-box testing
在<mark>知道程式所有內容</mark>的情況下所進行的測試 (通常由 developer 設計與執行)
+ **獨立路徑 independent path**
+ 說明:在程式流程圖中的,若其至少有一段路徑是前面找到的獨立路徑所沒有的,即為一條新的獨立路徑。
+ 範例:如下程式流程圖,總共有 4 條獨立路徑 (Path 4 應為 1-2-3-4-5-9)。

+ **<mark>第一種覆蓋:行數覆蓋</mark> line coverage**
+ 難度:Easy
+ 目標:<mark>測試執行時,只求能跑過程式的每一行</mark>
+ 範例:如下
雖然 line coverage 是 100% (確實 get_email 每一行都被執行過),\
但問題是,測試沒有測到 `is_cool_user == False` 的情況,\
因此 branch coverage 就沒有 100%。\
正好,此 MUT 的 bug 會在 `is_cool_user == False` 時發生 (user 為 None)。
```py
def get_email(is_cool_user: bool) -> str:
user: User | None = None
if is_cool_user:
user = User("John", "john0813@gmail.com")
return user.email
def test_get_email():
assert get_email(True) == "john0813@gmail.com"
```
+ **<mark>第二種覆蓋:分支覆蓋</mark> branch coverage**
+ 難度:Medium
+ 目標:<mark>測試執行時,只求能跑過程式中的所有分支</mark>
+ 範例:如下總共有 6 個分支


+ **<mark>第三種覆蓋:路徑覆蓋 path coverage</mark> (basis path testing)**
+ 難度:Hard
+ 目標:<mark>測試執行時,盡可能跑過程式的每個獨立路徑</mark> (一個獨立路徑對應著一個 test case)
+ 注意:<mark>即便通過路徑測試,也不代表程式一定不出錯</mark> (因為有些錯誤,可能要多跑幾次迴圈才會出現)

+ **循環複雜度 cyclomatic complexity**
+ 說明:路徑測試中,<mark>如果 test case 數量等於此值,可以保證所有程式都被測試覆蓋</mark> (upper bound)。
+ 關係:<mark>branch coverage ≤ cyclomatic complexity ≤ number of independent paths</mark>
+ 算法一:`CC(G) = Number(edges) - Number(nodes) + 2`
+ 如範例程式流程圖,可用此方法得知 CC = 11 - 9 + 2 = 4
+ 算法二 (適用無 goto):`CC = 條件敘述數 + 1` (switch-case 中每個 case 都算一個條件)
+ 如範例程式流程圖,可用此方法得知 CC = 3 + 1 = 4
## 整合測試 Integration Testing
+ 目的:檢驗軟體結構中<mark>各模組的的每個功能與性能介面功能是否正常</mark>
+ 步驟:根據整合測試計畫逐步將單元測試完成整合成系統

### 整合方式:top-down

### 整合方式:bottom-up

### 整合方式:sandwich

#### 流程


## 系統測試 System Testing
+ 目的:檢驗系統是否符合 SRS、滿足承諾的功能和服務、軟體本身性能、安全性、相容性等,這就是所謂的品質
+ 範例:

## 整合測試 Integration Testing
+ 目的:測試<mark>多個模組之間</mark>的互動和整合情況。它的重點在於驗證模組之間的接口是否能正確協作。
## 端到端測試 End-to-End Testing (E2E)
+ 目的:模擬使用者實際使用情境,測試從開始到結束的完整工作流程,目的是驗證不同系統或子系統之間的整合是否無縫運作 (<mark>涉及外部系統</mark>)。
## 回歸測試 Regression Testing
+ 目的:驗證<mark>新功能的添加</mark>、<mark>現有功能的修改</mark>,或系統修復後,<mark>是否對已有功能產生了非預期的影響</mark>。
### 冒煙測試 Smoke Testing
自動化測試時,通常須備好大量 test cases。而在<mark>資源不足</mark>的情況下,就可改採冒煙測試,只準備<mark>少量 test cases</mark> (比如 10 個),它會跑過最主要的功能和邏輯,沒跑過就代表冒煙。
# 非功能性測試 Non-Functional Testing
## 效能測試 Performance Testing
+ 目的:...
## 壓力測試 Stress Testing
+ 目的:透過超出正常負荷的方式操作系統,並測試其行為
## 驗收測試 Acceptance Testing
+ 目的:測試使用者體驗 (UX) 是否良好
+ 注意:使用者的要求經常超出最初的 SPEC,或者可能導致架構需調整或更改
## 使用者測試 User Testing
### Alpha 測試 alpha testing
+ 系統在<mark>開發團隊</mark>的<mark>自身環境</mark>中進行測試。
+ 大部分功能皆已開發完畢。
### Beta 測試 beta testing
+ 系統在<mark>使用者</mark>的<mark>實際環境</mark>中進行測試。
+ 主要 bug 皆已修復。