# 單元測試 Unit test --- ## 關於我 ![](https://i.imgur.com/QqOaRRR.png) ### Sid ---- 先後於台中 [Microprogram 微程式資訊]() 與 [Newegg 台灣新蛋]() 擔任前端工程師,負責使用前端技術開發與維護 Web 與 React Native App 相關服務。 ---- 現任職於 [Storipress](https://storipress.com/) 擔任前端工程師,除了前端功能的開發以外,在團隊中也負責前端專案 DevOps 優化的工作。 ---- 主要出沒地: - 台中「[Monospace 共同工作空間](https://www.facebook.com/monospace.tw/)」 - 個人部落格 [這可要好好管管 Sid.tw](https://sid.tw/) --- ## 大綱 - 測試概要 - 前端單元測試的說明 - 對組件進行測試 - Mock data --- ## 測試概要 ---- ### 什麼時候需要測試? 不論人工測試或自動化測試 ---- - 新功能開發 - Bug 修復 - 每一次部署前 ---- 如此高頻率的測試 手動測試無法涵蓋所有情境 ---- #### 自動化測試當然也無法涵蓋所有情境 測試的目的在防止已知的問題再次出現 ---- ### 前端自動化測試的主要類型 - Unit test 單元測試 - End-to-end test 端對端測試 ---- ![](https://i.imgur.com/HFubTky.png) - 圖片來源 : [Microservices Testing Strategies, Types & Tools: A Complete Guide](https://www.simform.com/blog/microservice-testing-strategies/) ---- #### 單元測試的優點: - 提高對程式改動的信心 - 快速理解函數作用 ---- #### 例如這樣的一段程式: ```typescript 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, ',') } ``` ---- ```typescript 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 中 ---- ![](https://i.imgur.com/WXfx2gG.png) ---- ### 測試檔案格式 從比較簡單的純 function 的測試開始 ---- 一個測試檔案可能會有像這樣的架構 ![](https://i.imgur.com/VLYOsrA.png) ---- ### Expect 與 Matcher 最常見的基本 Matcher ![](https://i.imgur.com/2vHgYCr.png) ---- 也有透過 Snapshot 來檢查的方式 ![](https://i.imgur.com/YMtBkeV.png) ---- #### 基於成本(時程)考量 測試不一定要完整的寫完測試案例: `toMatchSnapshot` --- ## 組件測試 ---- ### Testing Library 以接近真實的使用方式來進行測試 不去關注組件內部的狀態,重點關注在呈現結果 ![](https://i.imgur.com/fC6U76h.png) ---- ### 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 | ---- #### 使用門檻較高的選擇器 - [ByRole](https://testing-library.com/docs/queries/byrole) 需要對 HTML aria 有一定程度的理解 可參閱相關文件 [HTML aria #docconformance](https://www.w3.org/TR/html-aria/#docconformance) ---- #### 不得已的選擇 ![](https://i.imgur.com/JIVIKCF.png) ---- Query 的工具 [Testing Playground](https://testing-playground.com/) ![](https://i.imgur.com/CazXhQK.png) ---- #### fireEvent ![](https://i.imgur.com/UwJmzvk.png) ---- #### fireEvent 需注意 Code 中監聽的事件 ---- #### 更好的選擇 `@testing-library/user-event` [User Interactions](https://testing-library.com/docs/user-event/intro) ---- ```ts 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) ![](https://i.imgur.com/QbxgofR.png) ---- ### Demo [reactjs-vite-tailwindcss-boilerplate](https://github.com/joaopaulomoraes/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 ---- ```ts // 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](https://codesandbox.io/p/sandbox/testing-library-demo-7pnvpn) --- ## Q&A --- ## 謝謝大家
{"metaMigratedAt":"2023-06-18T03:22:01.082Z","metaMigratedFrom":"YAML","title":"單元測試 Unit test","breaks":true,"contributors":"[{\"id\":\"c94d6bee-5667-4118-951c-18b1911ba0f1\",\"add\":8769,\"del\":3107}]"}
    265 views