單元測試 Unit test


關於我

Sid


先後於台中 Microprogram 微程式資訊Newegg 台灣新蛋 擔任前端工程師,負責使用前端技術開發與維護 Web 與 React Native App 相關服務。


現任職於 Storipress 擔任前端工程師,除了前端功能的開發以外,在團隊中也負責前端專案 DevOps 優化的工作。


主要出沒地:


大綱

  • 測試概要
  • 前端單元測試的說明
  • 對組件進行測試
  • Mock data

測試概要


什麼時候需要測試?

不論人工測試或自動化測試


  • 新功能開發
  • Bug 修復
  • 每一次部署前

如此高頻率的測試
手動測試無法涵蓋所有情境


自動化測試當然也無法涵蓋所有情境

測試的目的在防止已知的問題再次出現


前端自動化測試的主要類型

  • Unit test 單元測試
  • End-to-end test 端對端測試


單元測試的優點:

  • 提高對程式改動的信心
  • 快速理解函數作用

例如這樣的一段程式:

function addDecimal(text?: string | number | null) {
  if (Number.isNaN(Number(text))) return '0.00'
  if (typeof text === 'number' || /\d+\.\d+/.test(String(text))) {
    text = Number(text).toFixed(2)
  }
  return Number(`${text ?? 0}`.replace(/\B(?=\d{2}$)/, '.'))
    .toFixed(2)
    .replace(/\B(?=(\d{3})+(?!\d))/g, ',')
}

test('addDecimal', () => {
  expect(addDecimal()).toBe('0.00')
  expect(addDecimal(null)).toBe('0.00')
  expect(addDecimal('abc')).toBe('0.00')
  expect(addDecimal('0')).toBe('0.00')
  expect(addDecimal('1')).toBe('1.00')
  expect(addDecimal('12')).toBe('12.00')
  expect(addDecimal('12345')).toBe('123.45')
  expect(addDecimal('123451')).toBe('1,234.51')
  expect(addDecimal('123.45')).toBe('123.45')
  expect(addDecimal('123.451')).toBe('123.45')
  expect(addDecimal(12345)).toBe('12,345.00')
  expect(addDecimal(123456)).toBe('123,456.00')
  expect(addDecimal(123.45)).toBe('123.45')
  expect(addDecimal(123.451)).toBe('123.45')
  expect(addDecimal(0)).toBe('0.00')
})


測試就是一份強制你定時更新的文件


單元測試的缺點:

  • 測試通過不代表功能正常
  • 單元測試可能會經常變動,增加維護成本
  • UI 的測試偵錯不夠直觀

E2E 測試的優點:

  • 可以確保整個應用程序的功能能夠正常工作
  • 可以幫助驗證應用程序的真實狀態,包括前端和後端的互動

E2E 測試的缺點:

  • 測試需要花費較長時間
  • E2E 測試的偵錯相對複雜
  • 撰寫 E2E 測試需要對架構有更全面的了解

測試的選擇

  • function
  • 組件
  • Hooks
  • 頁面

分享我們團隊 (新創團隊) 的協作流程:

  • Trunk Based Development
  • Unit test + E2E test
  • PR 同時進行 CI/CD
  • code review 審查

前端單元測試的撰寫


Vitest / Jest

  • Jest 是前端單元測試主流的測試框架之一,提供測試覆蓋率報告、快照測試等方便的功能

  • Vitest 提供了與 Jest 兼容的 API

    • Vite 中使用 Vitest 效能比 Jest 更好
    • Vitest 的設置整合在 Vite config 中


測試檔案格式

從比較簡單的純 function 的測試開始


一個測試檔案可能會有像這樣的架構


Expect 與 Matcher

最常見的基本 Matcher


也有透過 Snapshot 來檢查的方式


基於成本(時程)考量

測試不一定要完整的寫完測試案例: toMatchSnapshot


組件測試


Testing Library

以接近真實的使用方式來進行測試

不去關注組件內部的狀態,重點關注在呈現結果


Query

組件測試的第一步


可以用的選擇器

  • ByText
  • ByLabelText
  • ByPlaceholderText
  • ByDisplayValue
  • ByAltText
  • ByTitle
  • ByTestId

Single Element

Type of Query 0 Matches 1 Match >1 Matches Retry (Async/Await)
getBy Throw error Return element Throw error No
queryBy Return null Return element Throw error No
findBy Throw error Return element Throw error Yes

Multiple Elements

Type of Query 0 Matches 1 Match >1 Matches Retry (Async/Await)
getAllBy Throw error Return array Return array No
queryAllBy Return [] Return array Return array No
findAllBy Throw error Return array Return array Yes

使用門檻較高的選擇器

需要對 HTML aria 有一定程度的理解
可參閱相關文件 HTML aria #docconformance


不得已的選擇


Query 的工具

Testing Playground


fireEvent


fireEvent 需注意 Code 中監聽的事件


更好的選擇

@testing-library/user-event
User Interactions


test('userEvent example', () => {
  const input = screen.getByRole('textbox')
  userEvent.type(input, 'Hello, World!')
  expect(input).toHaveValue('Hello, World!')
})

test('fireEvent example', () => {
  const input = screen.getByRole('textbox')
  fireEvent.change(input, { target: { value: 'Hello, World!' } })
  expect(input).toHaveValue('Hello, World!')
})


Matcher (jest-dom)


Demo

reactjs-vite-tailwindcss-boilerplate - React Tailwindcss Boilerplate build with Vite

This is a boilerplate build with Vite, React 18, TypeScript, Vitest, Testing Library, TailwindCSS 3, Eslint and Prettier.


Mock API 資料


Mock Service Worker

  • 統一管理 Mock API 的回傳資料
  • 不需要修改 Code

// mocks/restful/unsplash.js

import { rest } from 'msw'
import data from './unsplash.json'

export const handler = rest
  .get(
    'https://api.unsplash.com/photos',
    (req, res, ctx) => res(ctx.json(data))
  )

Demo

Testing Library Demo - CodeSandbox


Q&A


謝謝大家

Select a repo