iT邦幫忙

2026 iThome 鐵人賽

DAY 10
0
Vibe Coding

Vibe Mode 開啟:30 天用 AI 打造網頁,邊做邊學 JavaScript系列 第 10 篇

# Day 10|睡個好覺的秘密武器!用 AI 自動生成 Vitest 單元與整合測試

  • 分享至 

  • xImage
  •  

今日目標

到了 Day 10,我們的「AI 個人財務追蹤器」已經擁有了 API 與 Day 9 加上去的 Zod 防禦機制。但身為一個現代工程師,最怕的就是「今天改了 A 邏輯,明天 B 功能默默壞掉」。

手寫測試很枯燥,但這正是 AI 的強項!今天我們要開啟 Vibe Mode,讓 AI 成為我們的 QA 自動化助手,使用超快的 Vitest 為交易紀錄 API(POST /api/transactions)撰寫 Unit Test(單元測試) 與 Integration Test(整合測試),把邊界條件全部鎖死!


Human vs. AI 實戰對照:測試案例

(Test Suite) 生成

1.給 AI 的指令 (Prompt)

我們正在使用 Vitest 測試 Next.js 14 App Router 的 API Route Handler。
請幫我為 `app/api/transactions/route.ts` 撰寫完整的單元與整合測試檔 `route.test.ts`:
1. 使用 `vi.mock()` Mock 掉 Supabase Client,避免測試連線到真實資料庫。
2. 測試案例需涵蓋:
   - [成功案例] 傳入合法資料,成功回傳 201 與新增資料。
   - [失敗案例] 傳入負數金額,經 Zod 驗證失敗回傳 400。
   - [失敗案例] Supabase 拋出錯誤時,回傳 500。
3. 語法請使用 Vitest 的 `describe`, `it`, `expect`, `vi`。

2. AI 產出的結果 (Code Snippet)

AI 很快地為我生成了測試框架:

// app/api/transactions/route.test.ts
import { describe, it, expect, vi } from 'vitest'
import { POST } from './route'

// Mock Supabase
vi.mock('@/utils/supabase/server', () => ({
  createClient: () => ({
    from: () => ({
      insert: () => ({
        select: () => ({ data: { id: '123', amount: 100 }, error: null })
      })
    })
  })
}))

describe('POST /api/transactions', () => {
  it('應成功建立交易紀錄', async () => {
    const req = new Request('http://localhost/api/transactions', {
      method: 'POST',
      body: JSON.stringify({
        amount: 100,
        type: 'expense',
        category: 'Food',
        date: new Date().toISOString()
      })
    })

    const res = await POST(req)
    const json = await res.json()

    expect(res.status).toBe(201)
    expect(json.success).toBe(true)
  })
})

3. 做出的修正與判斷 (Human Touch)

看似完美的測試檔,在執行 npx vitest 後卻立刻崩潰。檢查後會發現 AI 寫測試時常犯的兩個致命缺點:

 * Static Mock 無法測試失敗情境:AI 將 Supabase 的 Mock 寫死了(寫死永遠回傳 error: null)。這樣當我要測試「資料庫壞掉回傳 500」時,根本無法動態修改 Mock 的回傳值!
 * 缺乏測試間的狀態隔離:沒有使用 beforeEach(() => vi.clearAllMocks()),導致第一個測試呼叫 Mock 的紀錄會干擾第二個測試,引發測試間的連帶失敗(Test Pollution)。
💥 翻車與除錯過程 (Debug Experience)
🐛 踩坑:TypeError: Cannot read properties of undefined (reading 'select')
當我要求 AI 補上「資料庫出錯(500)」的測試時,它寫了以下動態 Mock 修改:
// AI 的錯誤嘗試
const mockInsert = vi.fn().mockReturnValue({ select: vi.fn() })
// 💥 這裡出事了!當 Promise 鏈拉長,Promise/Chaining 結構沒有被完整模擬,導致 select 回傳 undefined

因為 Next.js 裡的 Supabase 查詢使用了 Fluent API 鏈式呼叫(from().insert().select()),AI 生成的 Mock 只鏈結到第二層,後續的 .select() 直接吐出 undefined,引發 TypeError,測試根本沒走到我們的 API 邏輯就炸了!

引導修正 (Secondary Prompt)

不用補寫複雜的 Mock 結構,而是叫 AI 運用 vi.hoisted() 與彈性的 Mock Helper 重新構建鏈式呼叫:
你寫的 Supabase Mock 破壞了 Supabase 的 Fluent Chain (.from().insert().select()),導致 TypeError。
請改用 vi.hoisted 建立一個可動態控制回傳值的 mockSelect 與 mockInsert 函式,確保:

  1. 保留完整鏈式呼叫能力。
  2. 可以在單一 it() 測試中設定 .mockResolvedValueOnce() 以模擬資料庫報錯。
  3. 在每個測試執行前(beforeEach)自動重置所有 Mock。

最終修復後的測試檔

// app/api/transactions/route.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { POST } from './route'

// 1. 使用 vi.hoisted 確保 Mock 變數在模組載入前提升
const { mockSelect, mockInsert } = vi.hoisted(() => {
  const mockSelect = vi.fn()
  const mockInsert = vi.fn(() => ({ select: mockSelect }))
  return { mockSelect, mockInsert }
})

vi.mock('@/utils/supabase/server', () => ({
  createClient: () => ({
    from: () => ({ insert: mockInsert })
  })
}))

describe('POST /api/transactions Integration Tests', () => {
  beforeEach(() => {
    vi.clearAllMocks()
  })

  it('【成功】傳入正確資料應回傳 201', async () => {
    mockSelect.mockResolvedValueOnce({
      data: [{ id: 'tx_123', amount: 250, type: 'expense' }],
      error: null,
    })

    const req = new Request('http://localhost/api/transactions', {
      method: 'POST',
      body: JSON.stringify({
        amount: 250,
        type: 'expense',
        category: 'Dinner',
        date: new Date().toISOString(),
      }),
    })

    const res = await POST(req)
    const json = await res.json()

    expect(res.status).toBe(201)
    expect(json.data[0].id).toBe('tx_123')
  })

  it('【失敗】負數金額應被 Zod 擋下回傳 400', async () => {
    const req = new Request('http://localhost/api/transactions', {
      method: 'POST',
      body: JSON.stringify({ amount: -50, type: 'expense' }),
    })

    const res = await POST(req)
    const json = await res.json()

    expect(res.status).toBe(400)
    expect(json.errors.amount).toBeDefined()
    // 驗證 Zod 攔截成功,資料庫連叫都沒叫
    expect(mockInsert).not.toHaveBeenCalled()
  })

  it('【失敗】資料庫異常時應妥善捕捉回傳 500', async () => {
    mockSelect.mockResolvedValueOnce({
      data: null,
      error: { message: 'Database Connection Lost' },
    })

    const req = new Request('http://localhost/api/transactions', {
      method: 'POST',
      body: JSON.stringify({
        amount: 100,
        type: 'income',
        category: 'Salary',
        date: new Date().toISOString(),
      }),
    })

    const res = await POST(req)
    expect(res.status).toBe(500)
  })
})

JavaScript / TS 概念小知識

  • Why Vitest instead of Jest?
    在現代 Vite / Next.js 生態中,Vitest 逐漸成為主流。它原生支援 ES Modules (ESM) 與 TypeScript,無需複雜的 babel-jest 轉譯設定,執行速度比 Jest 快上數倍,且 API 幾乎與 Jest 100% 相容。

  • Mock Hoisting(模擬提升):
    在 JavaScript 中,vi.mock() 的呼叫會在任何 import 之前被「提升(Hoisted)」至檔案頂端執行。如果在 Mock 工廠函式內引用了頂層宣告的普通變數,會因為「尚未初始化」而丟出 ReferenceError。利用 vi.hoisted() 可以確保變數在提升階段就完成初始化。

結語

編寫測試是驗證專案健壯度的終極指針。
讓 AI 生成測試腳本不僅能省下 80% 寫重複 boilerplate 的時間,更能透過「看測試案例」反思自己的 API 設計是否有漏掉邊界。


上一篇
# Day 9 : 別讓防線大開!引導 AI 補足 Try-Catch、Zod 驗證與全域 Error Handling
下一篇
# Day 11:Mock Data 生成:如何利用 AI 快速打樣測試資料庫
系列文
Vibe Mode 開啟:30 天用 AI 打造網頁,邊做邊學 JavaScript 共 12 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言