---
tags: 簡報, JavaScript
---
# 單元測試 Unit test
---
## 關於我

### 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 端對端測試
----

- 圖片來源 : [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 中
----

----
### 測試檔案格式
從比較簡單的純 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 |
----
#### 使用門檻較高的選擇器
- [ByRole](https://testing-library.com/docs/queries/byrole)
需要對 HTML aria 有一定程度的理解
可參閱相關文件 [HTML aria #docconformance](https://www.w3.org/TR/html-aria/#docconformance)
----
#### 不得已的選擇

----
Query 的工具
[Testing Playground](https://testing-playground.com/)

----
#### fireEvent

----
#### 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)

----
### 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
---
## 謝謝大家