還記得 30 天前,我們從一個簡單的測試開始嗎?
test('myFirstTest', () => {
expect(1 + 1).toBe(2);
});
今天,我們不僅完成了完整的 Todo 應用,更重要的是建立了 TDD 思維。這趟旅程的終點,正是你成為更好開發者的起點。
今天我們要完成最後一塊拼圖:
第一週 - 測試基礎 ✅
├── 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();
}
// 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();
});
});
Day 1-10: 建立測試基礎
Day 11-17: 實戰練習
Day 18-27: 框架實戰
Day 28-30: 專業工程實踐
在真實專案中應用 TDD
持續優化測試策略
分享你的經驗
TDD 不只是一種測試方法,更是一種思維方式。這 30 天,我們學會了:
記住:好的測試不是為了通過,而是為了失敗時能告訴你原因。
30 天前,我們從零開始學習 TDD。今天,我們不僅完成了完整的專案,更重要的是建立了測試優先的開發思維。
這不是結束,而是開始。TDD 的精髓不在於技術,而在於持續追求更好的程式碼品質。每一個測試都是對未來的承諾,每一次重構都是對品質的堅持。
記住:
感謝你完成這 30 天的挑戰!現在,帶著 TDD 的力量,去創造更棒的軟體吧!
恭喜完成 30 天 TDD 實戰挑戰!
願測試與你同在,願程式碼永遠優雅!
感謝閱讀 30 天系列文章,歡迎持續關注 TDD 實踐之路!