Vue 測試策略與可維護性
測試不是為了追求覆蓋率數字,也不是把每個函式都測一遍。真正有價值的測試,是讓團隊在修改需求、重構組件、升級 Vue、調整 API 或修 bug 時,有足夠信心知道核心行為沒有壞掉。
一句話回答:Vue 測試策略要按風險分層:純邏輯與 Composable 用單元測試,元件互動與渲染狀態用組件測試,核心使用者流程用 E2E 測試;Mock 要放在系統邊界而不是把所有實作都 mock 掉,CI 要跑格式、型別、單元、組件與關鍵 E2E,讓測試成為重構安全網,而不是維護負擔。
一、 必考觀念
1. 測試策略不是工具清單
面試官問「Vue 專案怎麼做測試」,不要只回答:
- Vitest。
- Vue Test Utils。
- Cypress。
- Playwright。
- Jest。
這些只是工具。更重要的是你怎麼判斷:
什麼行為值得測?
什麼層級最適合測?
Mock 邊界在哪裡?
測試壞掉時能不能幫你定位問題?
測試會不會讓重構更困難?資深回答要從風險和維護成本出發。
2. 測試分層
常見分層:
| 層級 | 測什麼 | 特點 |
|---|---|---|
| 單元測試 | 純函式、Composable、資料轉換、權限判斷 | 快、穩、容易定位 |
| 組件測試 | props、emit、slot、表單互動、loading/error UI | 接近 Vue 使用方式 |
| Store 測試 | Pinia actions、狀態轉換、錯誤分支 | 適合複雜狀態 |
| API 邊界測試 | request client、錯誤標準化、DTO transform | 保護資料契約 |
| E2E 測試 | 登入、下單、建立資料、權限流程 | 最接近使用者,但慢且維護成本高 |
| 視覺回歸 | 設計系統、重要頁面截圖差異 | 適合 UI 穩定性要求高的產品 |
重點是:不要用 E2E 測所有東西,也不要只寫單元測試假裝流程安全。
3. 測試金字塔與測試獎盃
傳統測試金字塔:
少量 E2E
中量 Integration / Component
大量 Unit前端專案常會更重視組件測試,因為 UI 的價值不只在純函式,而是在使用者互動後畫面和事件是否正確。
可以這樣理解:
- 單元測試保護純邏輯。
- 組件測試保護 Vue 元件契約。
- E2E 測試保護跨頁核心流程。
不要把所有測試都塞在同一層。
二、 單元測試
1. 純函式最適合單元測試
例如權限判斷:
export function canAccess(
userPermissions: string[],
requiredPermissions: string[],
) {
return requiredPermissions.every(permission => {
return userPermissions.includes(permission)
})
}測試:
import { describe, expect, it } from 'vitest'
import { canAccess } from './permission'
describe('canAccess', () => {
it('returns true when user has all required permissions', () => {
expect(canAccess(['user:list', 'user:create'], ['user:list'])).toBe(true)
})
it('returns false when any required permission is missing', () => {
expect(canAccess(['user:list'], ['user:delete'])).toBe(false)
})
})這類測試價值很高,因為:
- 快。
- 不依賴 DOM。
- 不依賴網路。
- 錯了容易定位。
延伸讀:Vue 權限與路由守衛設計。
2. Composable 也適合單元測試
例如一個管理 loading/error 的 Composable:
import { ref } from 'vue'
export function useAsyncTask<T>(task: () => Promise<T>) {
const data = ref<T | null>(null)
const error = ref<Error | null>(null)
const loading = ref(false)
async function run() {
loading.value = true
error.value = null
try {
data.value = await task()
} catch (err) {
error.value = err as Error
} finally {
loading.value = false
}
}
return {
data,
error,
loading,
run,
}
}測試時可以直接呼叫:
it('sets data when task succeeds', async () => {
const { data, loading, run } = useAsyncTask(() => {
return Promise.resolve('ok')
})
await run()
expect(data.value).toBe('ok')
expect(loading.value).toBe(false)
})這比掛一個完整頁面再點按鈕更輕。
延伸讀:Composition API 實戰與 Composable 設計。
3. 不要測實作細節
不好的測試:
expect(wrapper.vm.internalFlag).toBe(true)如果 internalFlag 只是內部實作,重構時很容易讓測試壞掉,但使用者行為其實沒變。
更好的方向是測:
- 輸入什麼 props。
- 使用者做了什麼操作。
- 畫面呈現什麼。
- emit 了什麼事件。
- API 邊界被如何呼叫。
測試應該保護行為,不是把內部寫法凍住。
三、 組件測試
1. 組件測試要站在使用者角度
假設有一個搜尋元件:
<script setup lang="ts">
const keyword = defineModel<string>({
default: '',
})
const emit = defineEmits<{
search: [keyword: string]
}>()
</script>
<template>
<form @submit.prevent="emit('search', keyword)">
<input v-model="keyword" aria-label="關鍵字" />
<button type="submit">搜尋</button>
</form>
</template>測試可以關心:
- input 是否能輸入。
- submit 後是否 emit
search。 - emit payload 是否正確。
import { mount } from '@vue/test-utils'
import { describe, expect, it } from 'vitest'
import SearchForm from './SearchForm.vue'
describe('SearchForm', () => {
it('emits search keyword on submit', async () => {
const wrapper = mount(SearchForm)
await wrapper.get('input').setValue('vue')
await wrapper.get('form').trigger('submit')
expect(wrapper.emitted('search')).toEqual([[ 'vue' ]])
})
})這種測試保護的是元件對外契約。
延伸讀:v-model 雙向綁定的原理、Vue 與 TypeScript 實戰。
2. Props、Emit、Slot 是組件契約
組件測試通常不需要測所有 CSS,但要測契約:
| 契約 | 測試重點 |
|---|---|
| Props | 不同 props 下呈現是否正確 |
| Emit | 使用者操作後是否發出正確事件 |
| Slot | 預設插槽、具名插槽、作用域資料是否正確 |
| v-model | model 更新與 emit 是否一致 |
| Provide/Inject | 子樹上下文是否能協作 |
| 錯誤狀態 | loading、error、empty、disabled 是否正確 |
尤其是設計系統或共用元件,組件契約比內部實作更重要。
延伸讀:Vue 插槽設計:Slot、Scoped Slot 與 Headless Component。
3. 測試 Loading、Error、Empty 狀態
很多 bug 不在成功路徑,而在:
- API 載入中。
- API 失敗。
- 空資料。
- 無權限。
- 提交中。
- 重試後成功。
例如列表頁至少要測:
初次載入 -> 顯示 skeleton
成功且有資料 -> 顯示 rows
成功但空資料 -> 顯示 empty state
失敗 -> 顯示 error state 和 retry button延伸讀:Vue 錯誤處理與監控設計。
四、 Store、Router 與 API 測試
1. Pinia 測試要關心狀態轉換
例如 auth store:
export const useAuthStore = defineStore('auth', () => {
const token = ref<string | null>(null)
const user = ref<User | null>(null)
async function login(payload: LoginPayload) {
const result = await api.login(payload)
token.value = result.token
user.value = result.user
}
function logout() {
token.value = null
user.value = null
}
return {
token,
user,
login,
logout,
}
})測試重點:
- login 成功後 token/user 是否更新。
- login 失敗時狀態是否維持合理。
- logout 是否清乾淨。
- 權限切換是否清掉動態路由與快取。
延伸讀:Vue 狀態管理:Pinia、Vuex 與狀態歸屬。
2. Router Guard 測試要測決策
路由守衛容易出錯:
- 未登入是否導 login。
- 已登入是否進目標頁。
- 無權限是否導 403。
- login redirect 是否保留。
- 動態路由是否只載入一次。
這些邏輯可以抽成純函式:
export function resolveAuthRedirect(to: RouteLocationNormalized, auth: AuthState) {
if (to.meta.public) {
return true
}
if (!auth.token) {
return {
path: '/login',
query: {
redirect: to.fullPath,
},
}
}
return true
}先測純決策,再用少量整合測試確保 Router 接線正確。
3. API 測試要守住資料契約
前端常見錯誤是 API response 改了,頁面才炸。
可以測:
- request client 是否帶 token。
- 401 / 403 / 500 是否轉成標準 AppError。
- DTO 是否轉成前端 model。
- validation error 是否保留欄位錯誤。
- retry / timeout / abort 是否符合預期。
這類測試很適合放在 API layer,不要散在每個頁面。
延伸讀:Vue 表單、非同步狀態與資料流設計、Vue 請求、快取與體驗優化。
五、 E2E 測試
1. E2E 測核心流程,不測所有細節
E2E 測試最接近使用者,但也最慢、最容易受環境影響。
適合測:
- 登入。
- 權限流程。
- 建立 / 編輯 / 刪除核心資料。
- 結帳、付款、提交申請。
- 跨頁流程。
- 關鍵 regression bug。
不適合用 E2E 測:
- 每個 util function。
- 每個表單欄位細節。
- 每個組件所有 props 狀態。
- 所有錯誤碼排列組合。
那些應該下放到單元或組件測試。
2. E2E 要穩定資料與穩定選擇器
不穩定的 E2E 常見原因:
- 依賴真實外部服務。
- 測試資料互相污染。
- 用 CSS class 或文字硬抓元素。
- 等待時間用固定 sleep。
- 執行順序互相依賴。
比較好的做法:
- 使用測試專用帳號。
- 每個測試建立自己的資料。
- 測完清理資料或使用隔離資料庫。
- 使用
data-testid或 accessible selector。 - 等待明確 UI 狀態或 network 狀態。
- 測試可以單獨執行。
3. Mock 邊界要清楚
E2E 可以有兩種方向:
| 做法 | 適合 |
|---|---|
| 真後端 | 測完整整合、部署環境、真實 API |
| Mock API | 測前端流程、錯誤狀態、邊界案例 |
兩者都可以,但要知道自己測到什麼。
如果用 Mock API,測不到真後端契約;如果全走真後端,邊界狀態很難覆蓋,而且測試更慢更脆弱。
資深回答通常會說:核心 smoke flow 可接近真環境,前端狀態分支可用 Mock API 補足。
六、 Mock 策略
1. Mock 外部邊界,不要 Mock 自己要測的行為
合理 mock:
- API。
- Date / time。
- random id。
- localStorage。
- browser API。
- analytics。
- toast / modal service。
危險 mock:
- 把被測組件的核心子組件全部 stub 掉。
- 把 store action mock 到不再測狀態轉換。
- 把 router mock 到完全不測 navigation。
- Snapshot 一大片 HTML 當作主要驗證。
Mock 太多,測試會很穩,但也很空。
2. 測試替身要能表達意圖
例如 toast service 不需要真的顯示 UI,可以注入 fake:
const toast = {
success: vi.fn(),
error: vi.fn(),
}
mount(Component, {
global: {
provide: {
toast,
},
},
})然後測:
expect(toast.error).toHaveBeenCalledWith('提交失敗')這樣測的是「失敗時有通知使用者」,不是 toast 元件本身。
延伸讀:Vue 復用手段選型:Composable、Directive、Plugin、Mixin。
3. Snapshot 要克制
Snapshot 可以用,但不適合當主要測試策略。
問題是:
- 很容易變成機械更新。
- 差異不一定代表行為壞掉。
- 大 snapshot 很難 review。
- 對重構不友善。
更推薦測具體行為:
- 某段文字是否存在。
- 某個按鈕是否 disabled。
- 點擊後是否 emit。
- 錯誤時是否顯示 retry。
- API 是否被呼叫一次。
七、 CI 與品質門檻
1. CI 不只跑測試
成熟前端 CI 通常包含:
install
-> lint
-> typecheck
-> unit test
-> component test
-> build
-> key E2E smoke test每一層擋不同問題:
| 步驟 | 擋什麼 |
|---|---|
| lint | 風格、危險寫法、未使用變數 |
| typecheck | 型別錯誤、API 使用錯誤 |
| unit test | 純邏輯 regression |
| component test | 元件契約與互動 |
| build | 打包、路由、SSR/SSG 相容性 |
| E2E smoke | 主要流程是否可用 |
如果只在本機跑測試,團隊安全網會很不穩。
2. 測試分快慢
不是所有測試都要在每次 commit 全跑。
可以分:
- PR 必跑:lint、typecheck、unit、核心 component、build。
- 合併前或 nightly:完整 E2E、視覺回歸、效能 smoke。
- 發版前:核心流程、權限、支付、資料寫入。
這樣速度和安全性比較平衡。
3. 覆蓋率是參考,不是目標
覆蓋率太低通常是警訊,但覆蓋率高不代表安全。
更重要的是:
- 核心流程是否有測。
- 高風險邏輯是否有測。
- 錯誤分支是否有測。
- 測試是否穩定。
- 測試是否能幫助重構。
面試可以說:我會看 coverage,但不會為了數字去測 getter、setter 或無意義實作細節。
八、 可維護性設計
1. 可測試的程式通常更可維護
如果某段程式很難測,通常代表它太耦合:
- 元件同時處理 UI、API、權限、資料轉換。
- Composable 直接依賴全域單例。
- API response 在 template 裡直接使用。
- 表單狀態和 server data 混在一起。
- 副作用散在多個 watch 裡。
改善方式:
- 純邏輯抽函式。
- API client 統一。
- DTO transform 放資料層。
- UI 元件和業務流程拆開。
- 全域能力用 injection,測試時可替換。
2. 測試保護重構,不保護壞架構
測試能讓你安全修改,但不能替你設計好架構。
如果元件邊界混亂、狀態歸屬不清,測試會變得:
- 很難寫。
- 很容易壞。
- 需要大量 mock。
- 一改實作就整片紅。
所以測試和架構要一起看。
延伸讀:Vue 狀態管理:Pinia、Vuex 與狀態歸屬、Vue 復用手段選型:Composable、Directive、Plugin、Mixin。
3. 測試命名要描述行為
不好的命名:
it('test submit', () => {})比較好的命名:
it('keeps form draft when submit fails', () => {})
it('redirects unauthenticated users to login with redirect query', () => {})
it('shows retry button when dashboard request fails', () => {})好測試本身就是規格文件。
九、 追問題庫
Q1:Vue 專案測試怎麼分層?
可以回答:
- 純函式、資料轉換、權限判斷用單元測試。
- Composable 用單元測試或輕量組件包起來測。
- 共用元件和表單互動用組件測試。
- Pinia actions 和 Router Guard 測狀態轉換與決策。
- API client 測錯誤標準化與 DTO transform。
- 核心使用者流程用少量 E2E。
- CI 裡跑 lint、typecheck、test、build 和 smoke E2E。
Q2:組件測試應該測什麼?
測元件對外契約和使用者可觀察行為:
- props 對畫面的影響。
- 使用者操作後的 emit。
- slot 是否正確渲染。
- loading/error/empty 狀態。
- disabled、permission、validation 狀態。
不要過度測內部 ref、computed 名稱或方法呼叫順序。
Q3:E2E 為什麼不能寫太多?
因為 E2E 慢、維護成本高、定位問題困難,而且容易受資料與環境影響。
E2E 應該保護核心流程;細節分支要下放到單元測試和組件測試。
Q4:Mock API 好還是真後端好?
看目標。
Mock API 適合覆蓋前端狀態分支和錯誤場景;真後端適合測整合契約和部署環境。成熟專案通常兩者搭配。
Q5:覆蓋率多少才夠?
沒有固定答案。
可以設定團隊最低門檻,但更重要的是核心流程、高風險邏輯、錯誤分支是否被測到。覆蓋率是指標,不是目的。
十、 實作題
題目:為 Vue 後台設計測試策略
需求:
- 登入與權限很重要。
- 有大量表單。
- 有列表查詢、快取、錯誤回填。
- 有共用 UI 元件。
- 團隊常常重構。
可以這樣設計:
Unit
-> permission utils
-> DTO transform
-> validation rules
-> composables
Component
-> FormInput / Select / Modal
-> UserForm loading/error/validation
-> PermissionButton disabled / hidden
Store / Router
-> auth store login/logout
-> permission store route filtering
-> router guard redirect
API boundary
-> 401 / 403 / 422 / 500 normalize
-> requestId preserve
-> abort / retry behavior
E2E
-> login
-> create user
-> edit user
-> forbidden route
-> submit failure keeps draft
CI
-> lint
-> typecheck
-> unit + component
-> build
-> smoke E2E面試時補一句:我不會一開始追求所有頁面 100% coverage,而是先保護核心流程和容易壞的邊界,再逐步把 regression bug 轉成測試。
十一、 資深視角
1. 測試投資要看風險
高風險要多測:
- 金流。
- 權限。
- 登入。
- 資料刪除。
- 大型表單。
- 跨頁流程。
- 複雜資料轉換。
低風險可以少測:
- 靜態展示元件。
- 薄包裝元件。
- 幾乎沒有邏輯的頁面。
測試是成本,重點是把成本放在最值得保護的地方。
2. 測試要能支持重構
好的測試讓你可以:
- 把 Options API 改 Composition API。
- 把 Vuex 換 Pinia。
- 把 API client 重寫。
- 把表單拆成多個子元件。
- 升級 Vue 或建置工具。
如果只要重構內部實作測試就全部壞掉,代表測試太貼實作細節。
3. 測試文化比工具重要
團隊要約定:
- 新功能至少補關鍵測試。
- 修 bug 要補 regression test。
- flaky test 要優先修,不要習慣重跑。
- 測試失敗不能隨便跳過。
- 測試資料和 mock 要有人維護。
工具可以換,這些習慣才是真正的可維護性。
總結
| 問題 | 回答重點 |
|---|---|
| 測試分層 | Unit、Component、Store、API、E2E、視覺回歸 |
| 單元測試 | 純函式、Composable、資料轉換、權限判斷 |
| 組件測試 | Props、Emit、Slot、v-model、Loading/Error/Empty |
| E2E | 少量核心流程,避免測所有細節 |
| Mock | Mock 外部邊界,不要 Mock 掉被測行為 |
| CI | lint、typecheck、test、build、smoke E2E |
| 可維護性 | 測行為不測實作,保護重構安全 |
一句完整的面試回答可以是:
Vue 測試策略我會按風險分層。純函式、權限判斷、DTO transform、Composable 用單元測試;共用元件和表單互動用組件測試,重點測 props、emit、slot、v-model、loading/error/empty;Pinia 和 Router Guard 測狀態轉換與導航決策;API client 測 401、403、validation、timeout 等錯誤標準化;E2E 只覆蓋登入、權限、資料建立、提交等核心流程。Mock 要放在 API、時間、瀏覽器能力、toast 這些邊界,不要把被測行為 mock 掉。CI 裡跑 lint、typecheck、unit/component、build 和 smoke E2E。好的測試應該保護使用者行為和系統契約,讓團隊敢重構,而不是綁死內部實作。