March 7, 2025

前端測試指南-策略與實踐 (Chapter 1)


clipboard.png


🔍 Unit Tests Demo

主要驗證 function, method, class instance 程式碼最小單元功能確保獨立運作, 讓 input, output 可以符合預期.

const divideNumbers = (num, den) => {
  if (den === 0) {
    throw new Error('CAN NOT DIVIDE BY ZERO!!')
  }
  return num / den
} 

關於 divideNumbers 可能有下列四種:

所以測試實例應該涵蓋上述四種狀況

// example: 兩數整除, 得到一個整數
it('should return the correct integer when dividing two integers that are divisible.', ...)

🔍 Integeration Tests Demo

主要針對「合併的程式碼」做測試, 包含整合元件/套件等等, 以及取得整合元件正確協同運作, 在實際操作中獲得預期成果.

const HelloWorld = () => {
  const [msg, setMsg] = useState('')
  
  return (
    <div>
      <button 
        data-test-id="show-message-button"
        onClick={() => setMsg('Hello World!')}
      >
        Click!
      </button>
      <Text data-test-id="message">{message}</Text>
    </div>
  )
}
it('...', () => {
  const { getByTestId } = render(<HelloWorld />)
                                 
  // 點擊按鈕
  fireEvent.click(getByTestId('show-message-button'))
  
  // 檢視的顯示訊息是否符合預期
  expect(getByTestId('message')).toHaveTextContent("Hello World!")
})

此例整合 <HelloWorld>, <Text> 兩個組件的點擊與顯示訊息的流程.


🔍 End-to-End Test

模擬使用者操作流程去測試整個網站的內容, 是否符合實際情境使用.

🔍 Visual Test

是一種自動化測試方法,主要用來檢查應用程式的 UI 是否在不同版本或不同環境中保持一致。它通常用於 偵測 UI 變更,防止無意間的視覺錯誤,例如:

視覺測試的核心是「快照比對」,常見的方法有:

1️⃣ 像素對比(Pixel-by-Pixel)

2️⃣ DOM/樣式分析(DOM Snapshot)


測試類型說明優點缺點適用時機
單元測試(Unit Test)測試單一函式、元件或模組的行為,確保其獨立運作正常。- 快速執行,容易維護
- 針對特定功能測試,問題易於定位
- 無法測試元件之間的互動
- 可能無法捕捉 UI 錯誤
- 測試邏輯函式(如計算、資料處理)
- 測試 React/Vue 等 UI 元件(例如 Jest + React Testing Library
整合測試(Integration Test)測試多個模組如何互動,例如 API 服務與資料庫的連結。- 能檢測不同模組之間的問題
- 確保 API、前端、資料庫的協同運作
- 比單元測試慢
- 需要模擬資料或 API 回應(Mocking)
- 測試 API 的請求與回應
- 確保後端與前端能正確溝通
端對端測試(E2E Test)測試整個應用的運作,模擬使用者行為(如登入、操作 UI)。- 真實模擬使用者操作
- 確保所有系統部分正常運作
- 執行速度較慢
- 容易受 UI 變動影響,維護成本高
- 測試完整用戶流程(例如 Cypress、Playwright、Selenium
- 交易流程(如結帳、購物車)
視覺測試(Visual Test)透過快照比對 UI 畫面,檢查視覺變更是否符合預期。- 偵測 UI 變更,防止視覺錯誤
- 適用於多瀏覽器測試
- 容易受到小變化(如字型、陰影)的影響
- 可能產生誤報
- 測試 UI 是否有無意間的變更
- 適合使用 Chromatic、Applitools、Percy

🔍 GWT (Given-When-Then)

測試命名模式, 確保 test case 清晰度與一致性

🔍 it should

直接指名測試條件與預期結果

describe('', ()=>{
  it('should return 3 when 1 + 2')
})

🔍 3A (Arrange/Act/Assert)

對應到 Given-When-Then 的三個階段

重點: 清楚表達測試目的, 要做什麼, 預期結果


🔍 Mock Spy Double

用於模擬, 監視, 替代真實物件, 以便進行測試

🔍 Mock

模擬替代目標元件/第三方套件/函式庫, 以及 API 回應的物件或函式

允許開發者在不需要實際依賴的情況下, 對程式碼的一部分進行測試, 專注於測試中的特定功能或行為

利用 jest.mock 模擬指定路徑下的元件與內容, 後續只要遇到 <MyComponent> 都會以 <div>Hello World!</div> 取代之.

// 書內案例
jest.mock('./src/MyComponent', () => <div>Hello World!</div>)

Jest

// foo-bar-baz.js
export const foo = 'foo';
export const bar = () => 'bar';
export default () => 'baz';

//test.js
import defaultExport, {bar, foo} from '../foo-bar-baz';

jest.mock('../foo-bar-baz', () => {
  const originalModule = jest.requireActual('../foo-bar-baz');

  // Mock the default export and named export 'foo'
  return {
    __esModule: true, // 讓 jest 知道這是 ESM
    ...originalModule, // 把原有的內容 spread 進去
    default: jest.fn(() => 'mocked baz'), // 取代 模組 default 的內容
    foo: 'mocked foo', // 取代 {foo} 的內容
  };
});

test('should do a partial mock', () => {
  const defaultExportResult = defaultExport();
  expect(defaultExportResult).toBe('mocked baz'); // default 執行結果被修改為 'mocked baz'
  expect(defaultExport).toHaveBeenCalled(); // 確認有被呼叫

  expect(foo).toBe('mocked foo'); // foo 被修改為 mocked foo
  expect(bar()).toBe('bar'); // 沒有對 bar 處理事情
});

🔍 Spy

監視目標物件行為並記錄相關方法/參數/回傳值的物件, 過程不影響原有行為

it('should clean up the timer when unmount', () => {
  // 解構出 unmount 方法模擬 Timer 卸載
  const { unmount } = render(<Timer>) 
  // 監視全域(global)的 clearInterval 有沒有被 Timer 調用過
  const spyOnClearInterval = jest.spyOn(global, 'clearInterval'); 
  // 卸載組件 => 會觸發 clearInterval 被調用
  unmount()
  
  // 針對被調用結果寫 Assert 斷言
  expect(spyOnClearInterval).toHaveBeenCalledTimes(1);
})
const obj = {
  foo: (x) => x + 1,
};

test('jest.spyOn() example', () => {
  // 使用 spy 監視 obj.foo 方法
  const spy = jest.spyOn(obj, 'foo');

  // 調用 obj.foo 方法
  obj.foo(1);
  
  // 驗證 obj.foo 是否被調用過
  expect(spy).toHaveBeenCalled();
  expect(spy).toHaveBeenCalledWith(1); // 應該傳入 1 作為參數

  // 確保 spy 被呼叫後,會執行原本的方法
  expect(obj.foo(1)).toBe(2); // foo(1) 應該返回 2
});

🔍 Double

在測試中,“double” 是指「模擬對象」(Test Double)的概念。Test Double 是測試中用來替代實際對象的物件,通常是為了測試的目的而創建的,它可以模擬某些對象的行為或狀態。

// Dummy: 只是一個佔位符,通常不會被使用
const dummy = new Object(); // 只是填充,不會使用到的對象

// Fake: 一個簡單的替代實現,它的行為與真實對象相似
const fakeDatabase = {
  save: jest.fn(() => 'data saved'), // 一個假的數據庫方法
};

// Stubs: 會提供某些預設的行為,通常用來讓測試的控制更精確
const stub = jest.fn().mockReturnValue('stubbed result');
console.log(stub()); // 輸出 "stubbed result"

// Spies: 跟踪對象的函數調用,並記錄它的調用細節(比如調用了幾次,傳遞了哪些參數)。
...

// Mocks: 模擬對象的行為,並允許你定制其行為。這與 spy 類似,但提供更多功能。
...