跳至主要內容
Skip to content

Vue 測試策略與可維護性

測試不是為了追求覆蓋率數字,也不是把每個函式都測一遍。真正有價值的測試,是讓團隊在修改需求、重構組件、升級 Vue、調整 API 或修 bug 時,有足夠信心知道核心行為沒有壞掉。

一句話回答:Vue 測試策略要按風險分層:純邏輯與 Composable 用單元測試,元件互動與渲染狀態用組件測試,核心使用者流程用 E2E 測試;Mock 要放在系統邊界而不是把所有實作都 mock 掉,CI 要跑格式、型別、單元、組件與關鍵 E2E,讓測試成為重構安全網,而不是維護負擔。


一、 必考觀念

1. 測試策略不是工具清單

面試官問「Vue 專案怎麼做測試」,不要只回答:

  • Vitest。
  • Vue Test Utils。
  • Cypress。
  • Playwright。
  • Jest。

這些只是工具。更重要的是你怎麼判斷:

text
什麼行為值得測?
什麼層級最適合測?
Mock 邊界在哪裡?
測試壞掉時能不能幫你定位問題?
測試會不會讓重構更困難?

資深回答要從風險和維護成本出發。

2. 測試分層

常見分層:

層級測什麼特點
單元測試純函式、Composable、資料轉換、權限判斷快、穩、容易定位
組件測試props、emit、slot、表單互動、loading/error UI接近 Vue 使用方式
Store 測試Pinia actions、狀態轉換、錯誤分支適合複雜狀態
API 邊界測試request client、錯誤標準化、DTO transform保護資料契約
E2E 測試登入、下單、建立資料、權限流程最接近使用者,但慢且維護成本高
視覺回歸設計系統、重要頁面截圖差異適合 UI 穩定性要求高的產品

重點是:不要用 E2E 測所有東西,也不要只寫單元測試假裝流程安全。

3. 測試金字塔與測試獎盃

傳統測試金字塔:

text
少量 E2E
中量 Integration / Component
大量 Unit

前端專案常會更重視組件測試,因為 UI 的價值不只在純函式,而是在使用者互動後畫面和事件是否正確。

可以這樣理解:

  • 單元測試保護純邏輯。
  • 組件測試保護 Vue 元件契約。
  • E2E 測試保護跨頁核心流程。

不要把所有測試都塞在同一層。


二、 單元測試

1. 純函式最適合單元測試

例如權限判斷:

typescript
export function canAccess(
  userPermissions: string[],
  requiredPermissions: string[],
) {
  return requiredPermissions.every(permission => {
    return userPermissions.includes(permission)
  })
}

測試:

typescript
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:

typescript
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,
  }
}

測試時可以直接呼叫:

typescript
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. 不要測實作細節

不好的測試:

typescript
expect(wrapper.vm.internalFlag).toBe(true)

如果 internalFlag 只是內部實作,重構時很容易讓測試壞掉,但使用者行為其實沒變。

更好的方向是測:

  • 輸入什麼 props。
  • 使用者做了什麼操作。
  • 畫面呈現什麼。
  • emit 了什麼事件。
  • API 邊界被如何呼叫。

測試應該保護行為,不是把內部寫法凍住。


三、 組件測試

1. 組件測試要站在使用者角度

假設有一個搜尋元件:

vue
<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 是否正確。
typescript
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-modelmodel 更新與 emit 是否一致
Provide/Inject子樹上下文是否能協作
錯誤狀態loading、error、empty、disabled 是否正確

尤其是設計系統或共用元件,組件契約比內部實作更重要。

延伸讀:Vue 插槽設計:Slot、Scoped Slot 與 Headless Component

3. 測試 Loading、Error、Empty 狀態

很多 bug 不在成功路徑,而在:

  • API 載入中。
  • API 失敗。
  • 空資料。
  • 無權限。
  • 提交中。
  • 重試後成功。

例如列表頁至少要測:

text
初次載入 -> 顯示 skeleton
成功且有資料 -> 顯示 rows
成功但空資料 -> 顯示 empty state
失敗 -> 顯示 error state 和 retry button

延伸讀:Vue 錯誤處理與監控設計


四、 Store、Router 與 API 測試

1. Pinia 測試要關心狀態轉換

例如 auth store:

typescript
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 是否保留。
  • 動態路由是否只載入一次。

這些邏輯可以抽成純函式:

typescript
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:

typescript
const toast = {
  success: vi.fn(),
  error: vi.fn(),
}

mount(Component, {
  global: {
    provide: {
      toast,
    },
  },
})

然後測:

typescript
expect(toast.error).toHaveBeenCalledWith('提交失敗')

這樣測的是「失敗時有通知使用者」,不是 toast 元件本身。

延伸讀:Vue 復用手段選型:Composable、Directive、Plugin、Mixin

3. Snapshot 要克制

Snapshot 可以用,但不適合當主要測試策略。

問題是:

  • 很容易變成機械更新。
  • 差異不一定代表行為壞掉。
  • 大 snapshot 很難 review。
  • 對重構不友善。

更推薦測具體行為:

  • 某段文字是否存在。
  • 某個按鈕是否 disabled。
  • 點擊後是否 emit。
  • 錯誤時是否顯示 retry。
  • API 是否被呼叫一次。

七、 CI 與品質門檻

1. CI 不只跑測試

成熟前端 CI 通常包含:

text
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. 測試命名要描述行為

不好的命名:

typescript
it('test submit', () => {})

比較好的命名:

typescript
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 專案測試怎麼分層?

可以回答:

  1. 純函式、資料轉換、權限判斷用單元測試。
  2. Composable 用單元測試或輕量組件包起來測。
  3. 共用元件和表單互動用組件測試。
  4. Pinia actions 和 Router Guard 測狀態轉換與決策。
  5. API client 測錯誤標準化與 DTO transform。
  6. 核心使用者流程用少量 E2E。
  7. 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 元件。
  • 團隊常常重構。

可以這樣設計:

text
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少量核心流程,避免測所有細節
MockMock 外部邊界,不要 Mock 掉被測行為
CIlint、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。好的測試應該保護使用者行為和系統契約,讓團隊敢重構,而不是綁死內部實作。

延伸閱讀