🧪 React 測試與部署
📌 為什麼要測試?
「沒有測試的程式碼就是遺留程式碼」——Michael Feathers
測試確保:
- 功能正常運作
- 重構不破壞現有功能
- 團隊協作有信心
- 部署前抓到 bug
🧰 Jest + React Testing Library
Jest 是測試框架,React Testing Library 專注於使用者行為的測試方式。
# Vite 專案安裝
npm install -D vitest @testing-library/react @testing-library/jest-dom jsdom
基本元件測試
// Counter.jsx
import { useState } from 'react';
export function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p data-testid="count">計數:{count}</p>
<button onClick={() => setCount(c => c + 1)}>+1</button>
<button onClick={() => setCount(0)}>歸零</button>
</div>
);
}
// Counter.test.jsx
import { render, screen, fireEvent } from '@testing-library/react';
import { Counter } from './Counter';
describe('Counter 元件', () => {
test('初始值為 0', () => {
render(<Counter />);
expect(screen.getByTestId('count')).toHaveTextContent('計數:0');
});
test('點擊 +1 後計數增加', () => {
render(<Counter />);
const button = screen.getByText('+1');
fireEvent.click(button);
expect(screen.getByTestId('count')).toHaveTextContent('計數:1');
});
test('點擊歸零後回到 0', () => {
render(<Counter />);
fireEvent.click(screen.getByText('+1'));
fireEvent.click(screen.getByText('+1'));
fireEvent.click(screen.getByText('歸零'));
expect(screen.getByTestId('count')).toHaveTextContent('計數:0');
});
});
💡 React Testing Library 的理念: 不測試實作細節(state 值、元件內部邏輯),而是測試使用者看到什麼、做了什麼。 這和原生 JS 的 DOM 測試不同——React 元件的測試模擬的是使用者互動。
🌐 Mock API 與非同步測試
// UserProfile.jsx
export function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(setUser);
}, [userId]);
if (!user) return <p>載入中...</p>;
return <h1>{user.name}</h1>;
}
// UserProfile.test.jsx
import { render, screen, waitFor } from '@testing-library/react';
import { UserProfile } from './UserProfile';
// Mock fetch
beforeEach(() => {
global.fetch = vi.fn(() =>
Promise.resolve({
json: () => Promise.resolve({ name: '小明', email: 'ming@test.com' }),
})
);
});
afterEach(() => {
vi.restoreAllMocks();
});
test('載入使用者資料後顯示名稱', async () => {
render(<UserProfile userId={1} />);
// 等待非同步操作完成
expect(screen.getByText('載入中...')).toBeInTheDocument();
await waitFor(() => {
expect(screen.getByText('小明')).toBeInTheDocument();
});
// 驗證 fetch 被正確呼叫
expect(global.fetch).toHaveBeenCalledWith('/api/users/1');
});
🎭 E2E 測試(Playwright)
E2E 測試模擬真實使用者操作,在真實瀏覽器中執行。
npm install -D @playwright/test
npx playwright install
// e2e/todo.spec.js
import { test, expect } from '@playwright/test';
test.describe('待辦應用', () => {
test('可以新增和完成待辦事項', async ({ page }) => {
await page.goto('http://localhost:5173');
// 新增待辦
await page.fill('input[placeholder="輸入待辦事項..."]', '學 React');
await page.click('button:text("新增")');
// 確認待辦出現
await expect(page.locator('li')).toContainText('學 React');
// 點擊完成
await page.click('li:text("學 React")');
await expect(page.locator('li')).toHaveCSS(
'text-decoration', 'line-through'
);
});
test('空白待辦不可新增', async ({ page }) => {
await page.goto('http://localhost:5173');
await page.click('button:text("新增")');
await expect(page.locator('li')).toHaveCount(0);
});
});
📦 打包優化
// vite.config.js — 打包設定
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
build: {
rollupOptions: {
output: {
// 手動分割程式碼
manualChunks: {
vendor: ['react', 'react-dom'],
router: ['react-router-dom'],
redux: ['@reduxjs/toolkit', 'react-redux'],
},
},
},
// 壓縮設定
minify: 'terser',
sourcemap: false, // 生產環境關閉 sourcemap
},
});
# 分析打包大小
npm install -D rollup-plugin-visualizer
# 執行後會產生 stats.html,視覺化各套件佔比
🚀 部署到 Vercel / Netlify
Vercel(推薦用於 React/Next.js)
# 安裝 Vercel CLI
npm install -g vercel
# 部署
vercel
# 或連結 GitHub 自動部署:
# 1. 在 vercel.com 匯入 GitHub 專案
# 2. 設定 Build Command: npm run build
# 3. 設定 Output Directory: dist
# 4. 每次 push 到 main 自動部署
Netlify
# 安裝 Netlify CLI
npm install -g netlify-cli
# 建置並部署
npm run build
netlify deploy --prod --dir=dist
SPA 路由設定(重要!)
# 在 public/ 目錄建立 _redirects 檔案(Netlify)
/* /index.html 200
# 或 vercel.json(Vercel)
{
"rewrites": [
{ "source": "/(.*)", "destination": "/index.html" }
]
}
⚠️ SPA 路由注意:React Router 的路由是前端處理的, 直接訪問
/dashboard時伺服器找不到檔案會回 404。 上面的設定把所有路徑導向 index.html,讓 React Router 處理。
✅ 本章重點
| 測試類型 | 工具 | 範圍 |
|---|---|---|
| 單元測試 | Vitest/Jest | 個別函式/Hook |
| 元件測試 | React Testing Library | 單一元件行為 |
| E2E 測試 | Playwright/Cypress | 完整使用者流程 |
| 部署平台 | 特點 |
|---|---|
| Vercel | 最佳 Next.js 支援,自動 CI/CD |
| Netlify | 簡單快速,適合靜態站 |
| SPA 設定 | 所有路徑導向 index.html |