# 📖 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 皆已修復。
×
Sign in
Email
Password
Forgot password
or
Sign in via Google
Sign in via Facebook
Sign in via X(Twitter)
Sign in via GitHub
Sign in via Dropbox
Sign in with Wallet
Wallet (
)
Connect another wallet
Continue with a different method
New to HackMD?
Sign up
By signing in, you agree to our
terms of service
.