☕ NEW! 完成新手任務即可參加抽獎!LINE 星巴克禮券等你拿,名額有限!        🎉 推廣活動:邀請好友註冊 DevLearn,累積推薦抽 LINE 星巴克禮券! 活動詳情 →        🔥 活動期間 2026/4/1 - 5/31 |已有 0 人參加       
react 進階

🧪 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

💡 大家的想法 · 0

載入中...
💬 即時聊天室 🟢 0 人在線
😀 😎 🤓 💻 🎮 🎸 🔥
➕ 新問題
📋 我的工單
💬 LINE 社群
🔒
需要註冊才能使用此功能
註冊帳號即可解鎖測驗、遊戲、簽到、筆記下載等所有功能,完全免費!
免費註冊