iT邦幫忙

2025 iThome 鐵人賽

DAY 30
0

前言:終點即起點

還記得 30 天前,我們從一個簡單的測試開始嗎?

test('myFirstTest', () => {
  expect(1 + 1).toBe(2);
});

今天,我們不僅完成了完整的 Todo 應用,更重要的是建立了 TDD 思維。這趟旅程的終點,正是你成為更好開發者的起點。

本日目標 🎯

今天我們要完成最後一塊拼圖:

  • 將 Todo 應用部署到雲端
  • 設置 CI/CD 確保測試品質
  • 回顧 30 天的學習成果
  • 展望 TDD 的未來之路

一、30 天學習地圖回顧 🗺️

第一週 - 測試基礎 ✅
├── Day 01-03: 環境設置與 TDD 基礎
├── Day 04-06: 測試結構與組織
└── Day 07-10: 測試替身與覆蓋率

第二週 - 進階應用 ✅  
├── Day 11-14: 實戰練習基礎
└── Day 15-17: 進階實作與總結

第三週 - 框架測試 ✅
├── Day 18-21: React Testing Library
└── Day 22-24: 進階測試技巧

第四週+ - 整合實戰 ✅
├── Day 25-27: MSW 與 E2E 測試
├── Day 28-29: CI/CD 與監控
└── Day 30: 部署與總結 🎯 您在這裡!

二、部署前的最終檢查 ✅

在部署之前,讓我們確認所有的準備工作:

// 建立 tests/day30/deployment-readiness.test.ts
import { describe, test, expect } from 'vitest';

describe('Deployment Readiness', () => {
  test('allUnitTestsPass', async () => {
    const { results } = await import('../test-results.json');
    expect(results.numFailedTests).toBe(0);
    expect(results.numPassedTests).toBeGreaterThan(50);
  });

  test('coverageMeetsRequirements', async () => {
    const coverage = await import('../coverage/coverage-summary.json');
    expect(coverage.total.lines.pct).toBeGreaterThan(80);
  });

  test('bundleSizeIsAcceptable', async () => {
    const stats = await import('../dist/stats.json');
    const mainBundleSize = stats.assets.find(
      (asset: any) => asset.name.includes('main')
    )?.size || 0;
    expect(mainBundleSize).toBeLessThan(200000);
  });

  test('environmentVariablesAreSet', () => {
    expect(process.env.VITE_API_URL).toBeDefined();
    expect(process.env.VITE_APP_VERSION).toBeDefined();
  });
});

三、建置生產版本 🔨

// 建立 vite.config.prod.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],
  build: {
    minify: 'terser',
    terserOptions: {
      compress: {
        drop_console: true,
        drop_debugger: true,
      },
    },
    rollupOptions: {
      output: {
        manualChunks: {
          vendor: ['react', 'react-dom', 'react-router-dom'],
          utils: ['axios', 'date-fns'],
        },
      },
    },
    sourcemap: false,
  },
});

四、部署到雲端 ☁️

// 建立 vercel.json
{
  "buildCommand": "npm run build",
  "framework": "vite",
  "outputDirectory": "dist",
  "rewrites": [
    { "source": "/(.*)", "destination": "/index.html" }
  ],
  "headers": [
    {
      "source": "/api/(.*)",
      "headers": [
        { "key": "Cache-Control", "value": "no-cache" }
      ]
    },
    {
      "source": "/assets/(.*)",
      "headers": [
        { "key": "Cache-Control", "value": "public, max-age=31536000" }
      ]
    }
  ]
}

五、部署後的健康檢查 💡

// 建立 tests/e2e/health-check.spec.ts
import { test, expect } from '@playwright/test';

const PRODUCTION_URL = 'https://todo-app.vercel.app';

test.describe('Production Health Check', () => {
  test('applicationLoadsSuccessfully', async ({ page }) => {
    const response = await page.goto(PRODUCTION_URL);
    expect(response?.status()).toBe(200);
    await expect(page.locator('h1')).toContainText('Todo App');
  });

  test('canPerformCrudOperations', async ({ page }) => {
    await page.goto(PRODUCTION_URL);
    
    // Create
    await page.fill('[data-testid="todo-input"]', 'Test todo');
    await page.click('[data-testid="add-todo"]');
    await expect(page.locator('[data-testid="todo-item"]'))
      .toContainText('Test todo');
    
    // Update
    await page.click('[data-testid="edit-todo-0"]');
    await page.fill('[data-testid="edit-input"]', 'Updated');
    await page.click('[data-testid="save-edit"]');
    
    // Delete
    await page.click('[data-testid="delete-todo-0"]');
    await expect(page.locator('[data-testid="todo-item"]'))
      .not.toBeVisible();
  });

  test('performanceMetricsAreAcceptable', async ({ page }) => {
    await page.goto(PRODUCTION_URL);
    
    const metrics = await page.evaluate(() => {
      const nav = performance.getEntriesByType('navigation')[0] 
        as PerformanceNavigationTiming;
      return {
        domContentLoaded: nav.domContentLoadedEventEnd,
        loadComplete: nav.loadEventEnd,
      };
    });
    
    expect(metrics.domContentLoaded).toBeLessThan(3000);
    expect(metrics.loadComplete).toBeLessThan(5000);
  });
});

六、監控部署狀態 📈

// 建立 src/utils/monitoring.ts
export class ProductionMonitor {
  private static instance: ProductionMonitor;
  private metrics = new Map<string, any>();

  static getInstance(): ProductionMonitor {
    if (!this.instance) {
      this.instance = new ProductionMonitor();
    }
    return this.instance;
  }

  trackPerformance() {
    if ('PerformanceObserver' in window) {
      const observer = new PerformanceObserver((list) => {
        list.getEntries().forEach((entry) => {
          this.metrics.set(`perf_${entry.name}`, {
            duration: entry.duration,
            timestamp: Date.now(),
          });
        });
      });
      observer.observe({ entryTypes: ['measure', 'navigation'] });
    }
  }

  trackErrors() {
    window.addEventListener('error', (event) => {
      console.error('Production Error:', {
        message: event.message,
        source: event.filename,
        line: event.lineno,
      });
    });
  }
}

// 建立 src/main.tsx
import { ProductionMonitor } from './utils/monitoring';

if (import.meta.env.PROD) {
  const monitor = ProductionMonitor.getInstance();
  monitor.trackPerformance();
  monitor.trackErrors();
}

七、30 天學習成果總覽 🏆

測試能力成長

// Day 1 的我們
test('simpleAddition', () => {
  expect(1 + 1).toBe(2);
});

// Day 30 的我們  
describe('Todo Application', () => {
  beforeAll(async () => {
    await setupTestDatabase();
  });

  test('completeUserJourney', async () => {
    const user = userEvent.setup();
    render(<App />);
    
    // 登入流程
    await user.type(screen.getByLabelText('Email'), 'test@example.com');
    await user.click(screen.getByRole('button', { name: 'Login' }));
    
    // 創建 Todo
    await waitFor(() => 
      expect(screen.getByText('My Todos')).toBeInTheDocument()
    );
    await user.type(screen.getByPlaceholderText('What needs to be done?'), 
      'Learn TDD');
    await user.click(screen.getByRole('button', { name: 'Add' }));
    
    // 驗證結果
    expect(await screen.findByText('Learn TDD')).toBeInTheDocument();
  });
});

關鍵學習里程碑

  1. Day 1-10: 建立測試基礎

    • 學會寫測試、組織測試、使用測試替身
    • 掌握 TDD 紅綠重構循環
  2. Day 11-17: 實戰練習

    • 體驗完整的 TDD 開發流程
    • 建立測試優先的開發習慣
  3. Day 18-27: 框架實戰

    • 掌握 React Testing Library
    • 整合 MSW 進行 API 測試
  4. Day 28-30: 專業工程實踐

    • 建立 CI/CD 流程
    • 完成生產環境部署

八、關鍵學習里程碑 🌟

持續學習與實踐

  1. 在真實專案中應用 TDD

    • 從小功能開始
    • 逐步擴展到整個專案
  2. 持續優化測試策略

    • 定期檢視測試覆蓋率
    • 保持測試的可維護性
  3. 分享你的經驗

    • 撰寫技術文章
    • 在團隊中推廣 TDD

九、TDD 帶來的改變 💡

TDD 不只是一種測試方法,更是一種思維方式。這 30 天,我們學會了:

  • 📝 先思考再編碼
  • 🔄 持續重構改進
  • 🛡️ 建立信心和安全網
  • 🚀 提升開發效率

記住:好的測試不是為了通過,而是為了失敗時能告訴你原因。

總結:TDD 之旅的延續

30 天前,我們從零開始學習 TDD。今天,我們不僅完成了完整的專案,更重要的是建立了測試優先的開發思維。

這不是結束,而是開始。TDD 的精髓不在於技術,而在於持續追求更好的程式碼品質。每一個測試都是對未來的承諾,每一次重構都是對品質的堅持。

記住:

  • 紅燈 提醒我們先思考再行動
  • 綠燈 給我們前進的信心
  • 重構 讓程式碼保持優雅

感謝你完成這 30 天的挑戰!現在,帶著 TDD 的力量,去創造更棒的軟體吧!

下一步行動 📋

  • [ ] 部署你的 Todo 應用
  • [ ] 分享你的學習心得
  • [ ] 挑選下一個 TDD 專案
  • [ ] 持續精進測試技巧
  • [ ] 成為 TDD 的推廣者

恭喜完成 30 天 TDD 實戰挑戰!

願測試與你同在,願程式碼永遠優雅!


感謝閱讀 30 天系列文章,歡迎持續關注 TDD 實踐之路!


上一篇
Day 29 - 整合實戰 🚀
系列文
React TDD 實戰:用 Vitest 打造可靠的前端應用30
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言