# 📖 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> (盡可能早地完成測試)。 + 測試左移前 ![before-testing-left-shift](https://hackmd.io/_uploads/rycfgBx31e.png) + 測試左移後 ![after-testing-left-shift](https://hackmd.io/_uploads/B1WEeHg2Jx.png) ### 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 ![management-hierarchy](https://hackmd.io/_uploads/HkUIL2F51g.png) ### 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 ![v-model](https://hackmd.io/_uploads/BJXRIhYqkl.png) ## 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。 ![TDD](https://miro.medium.com/v2/resize:fit:1400/format:webp/1*FGH_G1cQgpgilniCKY8NUQ.png) ## Test Plan ![test-plan-content-structure](https://hackmd.io/_uploads/Hyshh3tckx.png) ![the-goals-of-test-plan](https://hackmd.io/_uploads/HkIe6hK9ye.png) ## 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> + 除了選擇在等價區塊中的邊界值,也需要測試區塊外的邊界值 + 等價區間只以輸入建立測資,邊界值分析同時使用輸入和輸出 ![black-box-testing-1](https://hackmd.io/_uploads/HkIBuBoqye.png) ![black-box-testing-2](https://hackmd.io/_uploads/HJxoevj5ke.png) ### 白箱測試 white-box testing 在<mark>知道程式所有內容</mark>的情況下所進行的測試 (通常由 developer 設計與執行) + **獨立路徑 independent path** + 說明:在程式流程圖中的,若其至少有一段路徑是前面找到的獨立路徑所沒有的,即為一條新的獨立路徑。 + 範例:如下程式流程圖,總共有 4 條獨立路徑 (Path 4 應為 1-2-3-4-5-9)。 ![independent-path](https://hackmd.io/_uploads/SJLskuocJg.png) + **<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 個分支 ![branch-coverage](https://hackmd.io/_uploads/Hkiz0Do51l.png) ![branch-coverage](https://hackmd.io/_uploads/H1LhYtjckg.png) + **<mark>第三種覆蓋:路徑覆蓋 path coverage</mark> (basis path testing)** + 難度:Hard + 目標:<mark>測試執行時,盡可能跑過程式的每個獨立路徑</mark> (一個獨立路徑對應著一個 test case) + 注意:<mark>即便通過路徑測試,也不代表程式一定不出錯</mark> (因為有些錯誤,可能要多跑幾次迴圈才會出現) ![program-flow-graph](https://hackmd.io/_uploads/SkKeFDsqyx.png) + **循環複雜度 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> + 步驟:根據整合測試計畫逐步將單元測試完成整合成系統 ![integration-testing](https://hackmd.io/_uploads/HkNoRtscJl.png) ### 整合方式:top-down ![top-down](https://hackmd.io/_uploads/ByL0TYi51x.png) ### 整合方式:bottom-up ![bottom-up](https://hackmd.io/_uploads/B1h4CFs9yx.png) ### 整合方式:sandwich ![sandwich](https://hackmd.io/_uploads/B1GuAYiq1e.png) #### 流程 ![procedure-1](https://hackmd.io/_uploads/Hylee5o51e.png) ![procedure-2](https://hackmd.io/_uploads/rJbs1qi9yx.png) ## 系統測試 System Testing + 目的:檢驗系統是否符合 SRS、滿足承諾的功能和服務、軟體本身性能、安全性、相容性等,這就是所謂的品質 + 範例: ![system-test-case-1](https://hackmd.io/_uploads/HJ9qQqj9yl.png) ## 整合測試 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 皆已修復。