Try   HackMD

📖 Testing

Toolkits

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 與其對應的 expected output

test case

測試案例。
它應當描述當系統在一個特定條件 (condition) 下時預期的反應
通常我們可以根據測試案例的條件,來決定 test data 的具體值。

  • 例如
    根據一個登入功能,可以設計一個測試案例:
    當使用者輸入正確的帳號和密碼時,應該成功登入。

negative test case

一種反向的測試案例。
它通常會給定 bad test input,看看 AUT / SUT 有何反應。
也就是說,這種測試案例是在考驗系統的例外處理做得好不好

  • 補充
    這個環節通常可以觀察出你的社會化程度
    因為人心是險惡的,你永遠不知道使用者用什麼神祕姿勢在敲鍵盤 (bad input 千奇百怪);
    因為公司的軟體服務是堅若磐石的,面對邪惡 bad input 絕不停機屈服。
  • 記住
    在多人團隊裡,不做例外處理的那個 developer 簡直就是老鼠屎

test suite

Test Case 的集合。

true oracle

已存在一個軟體,能根據指定輸入,輸出絕對正確的結果
這個軟體就被稱作 true oracle (在現實情境中,完美的幾乎不存在)。
有了他,你的 test case generation 就很方便。
實例:一個運作多年、人人稱羨的成熟系統,通常就逼近為一個 true oracle。
(雖然它可能是個舊系統,因效能問題要進行 refactor。但是我們在開發新系統時,還是可以拿它做驗證)

partial oracle

已存在一個特定算法,能根據輸入和輸出,來告訴你輸出是否正確
這個特定算法就被稱作 partial oracle (僅在簡單場景下存在)。
有了他,你的 test case generation 就很方便。
實例:比如說檢驗排序算法的正確性,只要檢查左邊的數字是否必定大於右邊,就好了。

testing left-shift

研究指出:

  • 引入 BUG 最常發生在:開發階段 (Coding)。若此時修正,成本低。
  • 發現 BUG 最常發生在:系統測試階段 (System Test)。若此時修正,成本高。

而進行測試可以發現 BUG,
因此為了降低修正成本,我們需要將測試左移 (盡可能早地完成測試)。

  • 測試左移前
    Image Not Showing Possible Reasons
    • The image was uploaded to a note which you don't have access to
    • The note which the image was originally uploaded to has been deleted
    Learn More →
  • 測試左移後
    Image Not Showing Possible Reasons
    • The image was uploaded to a note which you don't have access to
    • The note which the image was originally uploaded to has been deleted
    Learn More →

incoming dependency

給定輸入的資料,通常是經由先前的操作而被動流入 unit of work 的資料。
例如:query 資料庫的資料、API 的回應。

outgoing dependency

呼叫外部函式,這是 unit of work 的一種 exit point。
例如:寫入資料庫、呼叫 API 或 webhook。

stub | mock | spy

類型 定義 目的 範例
stub 假的輸入,模擬實作並取代原本的實作 隔離 incoming dependency 假造的測試資料、物件或函式
mock 假的輸出,模擬實作並取代原本的實作 隔離 outgoing dependency 呼叫假的服務、寫入假的資料庫
spy 監控呼叫,模擬實作但不取代原本的實作 記錄互動 是否被正確呼叫
communicate
assert
CUT / SUT
stub
test
communicate
assert
CUT / SUT
spy
test

Rules

F.I.R.S.T 原則

F.I.R.S.T Principles of Unit Testing

  1. Fast快速的
  2. Independent獨立的 測試之間要相互獨立,如果互相依賴的話,一個測試失敗會影響其他測試也都失敗
  3. Repeatable可重複的 要在任何環境都可重複執行
  4. Self-Validating可自驗證的 可從 report 直接了解失敗原因
  5. Timely及時的 最好是在寫程式之前先寫測試 (TDD 概念)

3A rule

考量 Independent 時遵守該原則

  1. Arrange (Given)
    建立此測試案例需要的初始值,和思考好的命名和變數命稱來讓測試更容易理解
  2. Act (When)
    呼叫目標方法
  3. Assert (Then)
    驗證是否符合預期

Management

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

Tester

  • 根據 test spec、test plan,編寫 test case
  • 事後發現缺陷
  • 品質是檢驗出來的
  • 測試只是幫助我們了解品質的現況
  • 專注於測試的具體實現
  • 近年來 programmer 也需扮演 tester 的角色

QA

  • 編寫 test spectest planautomation
  • 事前預防缺陷
  • 品質是計畫、設計、建置出來的
  • 思考什麼樣的品質能達到公司的目的
  • 專注於產品需求的全貌
  • 品質的觀念帶給 developer team
  • 不能歸 developer team 管轄,如同司法部門不能在行政部門底下

SDET

  • Software Development Engineer in Test
  • 建立測試架構平台
  • 設計與優化自動化測試架構

QA Manager

  • 策略、規劃、部門以及人員發展

V model

v-model

Test-Driven Development (TDD)

Reference

可參考:🎬 ArjanCodes - Test-Driven Development In Python

核心理念

先寫測試,再做開發

優點

  • 可確保每次改動的品質,不會改 A 壞 B
  • 可利用 test case 釐清使用情境,減少溝通成本
  • 新人可透過 test case 更了解每個 function 在做什麼事,包含呼叫情境、傳入引數及預期結果

開發三步驟

  • 第一步 紅燈 RED

    寫測試,並且執行測試 (此時 test case 應當全 FAILED。很正常,因為你還沒實作介面)

  • 第二步 綠燈 GREEN

    寫程式。目的是要讓所有 test case PASSED。

  • 第三步 重構 REFACTOR

    重構程式。增加程式碼的可讀性、記憶體的優化、運算效能的優化。與此同時須保持所有 test case PASSED。

TDD

Test Plan

test-plan-content-structure
the-goals-of-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:預期產生什麼結果
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);
    }
}
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

不知道程式內容的情況下所進行的測試 (通常需要 SDD)

  • 等價區間 equivalence partitioning

    • 同等價區間內的 test input,對於驗證程式的正確性,擁有相同的價值

      範例:假設需要輸入 username 向系統註冊帳號,username 必須長度在 3~6 之間,此時即出現 3 個等價區間,一個是小於 3,一個介於 3~6,一個是大於 6。
      註:這也是不需把所有 test input 排列組合都丟進來測試的本質原因

    • 每個等價區間需對應一個 test case

  • 邊界值分析 boundary value analysis

    • 比起在特定範圍中央,錯誤更容易在輸出入資料的邊界發生
    • 除了選擇在等價區塊中的邊界值,也需要測試區塊外的邊界值
    • 等價區間只以輸入建立測資,邊界值分析同時使用輸入和輸出

    black-box-testing-1
    black-box-testing-2

白箱測試 white-box testing

知道程式所有內容的情況下所進行的測試 (通常由 developer 設計與執行)

  • 獨立路徑 independent path

    • 說明:在程式流程圖中的,若其至少有一段路徑是前面找到的獨立路徑所沒有的,即為一條新的獨立路徑。
    • 範例:如下程式流程圖,總共有 4 條獨立路徑 (Path 4 應為 1-2-3-4-5-9)。
      independent-path
  • 第一種覆蓋:行數覆蓋 line coverage

    • 難度:Easy
    • 目標:測試執行時,只求能跑過程式的每一行
    • 範例:如下
      雖然 line coverage 是 100% (確實 get_email 每一行都被執行過),
      但問題是,測試沒有測到 is_cool_user == False 的情況,
      因此 branch coverage 就沒有 100%。
      正好,此 MUT 的 bug 會在 is_cool_user == False 時發生 (user 為 None)。
      ​​​​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"
      
  • 第二種覆蓋:分支覆蓋 branch coverage

    • 難度:Medium
    • 目標:測試執行時,只求能跑過程式中的所有分支
    • 範例:如下總共有 6 個分支
      branch-coverage

      branch-coverage
  • 第三種覆蓋:路徑覆蓋 path coverage (basis path testing)

    • 難度:Hard
    • 目標:測試執行時,盡可能跑過程式的每個獨立路徑 (一個獨立路徑對應著一個 test case)
    • 注意:即便通過路徑測試,也不代表程式一定不出錯 (因為有些錯誤,可能要多跑幾次迴圈才會出現)
      program-flow-graph
  • 循環複雜度 cyclomatic complexity

    • 說明:路徑測試中,如果 test case 數量等於此值,可以保證所有程式都被測試覆蓋 (upper bound)。
    • 關係:branch coverage ≤ cyclomatic complexity ≤ number of independent paths
    • 算法一:CC(G) = Number(edges) - Number(nodes) + 2
      • 如範例程式流程圖,可用此方法得知 CC = 11 - 9 + 2 = 4
    • 算法二 (適用無 goto):CC = 條件敘述數 + 1 (switch-case 中每個 case 都算一個條件)
      • 如範例程式流程圖,可用此方法得知 CC = 3 + 1 = 4

整合測試 Integration Testing

  • 目的:檢驗軟體結構中各模組的的每個功能與性能介面功能是否正常
  • 步驟:根據整合測試計畫逐步將單元測試完成整合成系統

integration-testing

整合方式:top-down

top-down

整合方式:bottom-up

bottom-up

整合方式:sandwich

sandwich

流程

procedure-1
procedure-2

系統測試 System Testing

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

  • 範例:

    system-test-case-1

整合測試 Integration Testing

  • 目的:測試多個模組之間的互動和整合情況。它的重點在於驗證模組之間的接口是否能正確協作。

端到端測試 End-to-End Testing (E2E)

  • 目的:模擬使用者實際使用情境,測試從開始到結束的完整工作流程,目的是驗證不同系統或子系統之間的整合是否無縫運作 (涉及外部系統)。

回歸測試 Regression Testing

  • 目的:驗證新功能的添加現有功能的修改,或系統修復後,是否對已有功能產生了非預期的影響

冒煙測試 Smoke Testing

自動化測試時,通常須備好大量 test cases。而在資源不足的情況下,就可改採冒煙測試,只準備少量 test cases (比如 10 個),它會跑過最主要的功能和邏輯,沒跑過就代表冒煙。

非功能性測試 Non-Functional Testing

效能測試 Performance Testing

  • 目的:

壓力測試 Stress Testing

  • 目的:透過超出正常負荷的方式操作系統,並測試其行為

驗收測試 Acceptance Testing

  • 目的:測試使用者體驗 (UX) 是否良好
  • 注意:使用者的要求經常超出最初的 SPEC,或者可能導致架構需調整或更改

使用者測試 User Testing

Alpha 測試 alpha testing

  • 系統在開發團隊自身環境中進行測試。
  • 大部分功能皆已開發完畢。

Beta 測試 beta testing

  • 系統在使用者實際環境中進行測試。
  • 主要 bug 皆已修復。