# Day 35:TDD 實作——從浮點數精度到架構重構的完整歷程 **Published by:** [雞蛋糕的前端修煉屋](https://paragraph.com/@gcake/) **Published on:** 2026-01-09 **URL:** https://paragraph.com/@gcake/day-35 ## Content 日期:2026/01/09 主題:TDD 超小步推進、測試設計方法、Number.EPSILON、架構設計📝 概述本筆記記錄一個真實的 TDD 開發流程,包含:使用 Uncle Bob 的超小步 TDD 實作複雜邏輯系統性測試設計方法(等價類劃分、邊界值分析)JavaScript 浮點數精度問題的發現與解決架構設計缺陷的發現與重構實戰案例:實作一個「指數成長 + 線性近似」的計算器(生物細菌分裂模擬)🎯 Part 1: 問題定義需求描述實作一個計算函數,模擬細菌分裂實驗:假設條件: 1. 沒有細菌死亡 2. 初始有 b 隻細菌 3. 每經過 20 分鐘,細菌數量翻倍(指數成長) 4. 不足 20 分鐘的部分用線性成長近似 輸入:initialBacteria(初始數量),minutesPassed(經過分鐘) 輸出:最終細菌數量(無條件捨去小數) 核心數學公式設 m 為經過的分鐘數,b 為初始細菌數:完整週期數:( \text{fullCycles} = \lfloor m / 20 \rfloor )完整週期後細菌數:( \text{afterFullCycles} = b \times 2^{\text{fullCycles}} )剩餘分鐘:( \text{remainingMinutes} = m \mod 20 )線性成長增量:( \text{linearGrowth} = \text{afterFullCycles} \times (\text{remainingMinutes} / 20) )最終細菌數:( \lfloor \text{afterFullCycles} + \text{linearGrowth} \rfloor )範例計算// 範例 1:0 分鐘 calculateBacteriaGrowth(100, 0) → 100 // 範例 2:20 分鐘(1 個完整週期) 100 × 2^1 = 200 // 範例 3:10 分鐘(半個週期) 基數:100 × 2^0 = 100 線性成長:100 × (10/20) = 50 總數:100 + 50 = 150 // 範例 4:30 分鐘(1 個完整週期 + 10 分鐘) 基數:100 × 2^1 = 200 線性成長:200 × (10/20) = 100 總數:200 + 100 = 300 🔄 Part 2: TDD 超小步推進實戰Uncle Bob 的 TDD 三定律根據 Clean Coder Blog:第一定律:在寫失敗測試之前,不得寫任何生產程式碼第二定律:測試程式碼只寫到剛好失敗為止第三定律:生產程式碼只寫到剛好讓當前失敗測試通過為止循環時間:每個 Red-Green-Refactor 循環約 20-40 秒循環 1:邊界測試(0 分鐘)🔴 Red:寫失敗測試// calculateBacteriaGrowth.test.js import { describe, it, expect } from 'vitest'; import { calculateBacteriaGrowth } from './calculateBacteriaGrowth.js'; describe('calculateBacteriaGrowth', () => { it('0 分鐘後,應回傳初始細菌數量', () => { expect(calculateBacteriaGrowth(10, 0)).toBe(10); }); }); // 執行:npx vitest // 結果:❌ FAIL - Cannot find module 🟢 Green:寫最簡單能通過的程式碼// calculateBacteriaGrowth.js export function calculateBacteriaGrowth(initialBacteria, minutesPassed) { return initialBacteria; // 最簡單的實作 } // 執行:npx vitest // 結果:✅ PASS (1/1) 🔵 Refactor:無需重構時間:30 秒循環 2:完整週期(20 分鐘)🔴 Red:加入新測試it('20 分鐘後,應回傳初始細菌數量的 2 倍', () => { expect(calculateBacteriaGrowth(10, 20)).toBe(20); }); // 結果:❌ FAIL // Expected: 20 // Received: 10 🟢 Green:實作指數成長export function calculateBacteriaGrowth(initialBacteria, minutesPassed) { const cycleMinutes = 20; const growthRate = 2; const fullCycles = Math.floor(minutesPassed / cycleMinutes); return initialBacteria * (growthRate ** fullCycles); } // 結果:✅ PASS (2/2) 🔵 Refactor:提取魔術數字為常數const CYCLE_MINUTES = 20; const GROWTH_RATE = 2; export function calculateBacteriaGrowth(initialBacteria, minutesPassed) { const fullCycles = Math.floor(minutesPassed / CYCLE_MINUTES); return initialBacteria * (GROWTH_RATE ** fullCycles); } 時間:40 秒循環 3:線性成長(10 分鐘)🔴 Red:測試半個週期it('10 分鐘後,應使用線性近似計算', () => { expect(calculateBacteriaGrowth(100, 10)).toBe(150); // 計算:100 + (100 × 10/20) = 150 }); // 結果:❌ FAIL // Expected: 150 // Received: 100 🟢 Green:加入線性計算const CYCLE_MINUTES = 20; const GROWTH_RATE = 2; export function calculateBacteriaGrowth(initialBacteria, minutesPassed) { const fullCycles = Math.floor(minutesPassed / CYCLE_MINUTES); const fullCycleMultiplier = GROWTH_RATE ** fullCycles; const remainingMinutes = minutesPassed % CYCLE_MINUTES; const linearGrowthMultiplier = fullCycleMultiplier * (remainingMinutes / CYCLE_MINUTES); const totalGrowth = initialBacteria * (fullCycleMultiplier + linearGrowthMultiplier); return Math.floor(totalGrowth); } // 結果:✅ PASS (3/3) 🔵 Refactor:改善變數命名const CYCLE_MINUTES = 20; const GROWTH_RATE = 2; export function calculateBacteriaGrowth(initialBacteria, minutesPassed) { // 計算完整週期數 const fullCycles = Math.floor(minutesPassed / CYCLE_MINUTES); const exponentialMultiplier = GROWTH_RATE ** fullCycles; // 計算剩餘分鐘的線性成長 const remainingMinutes = minutesPassed % CYCLE_MINUTES; const linearMultiplier = exponentialMultiplier * (remainingMinutes / CYCLE_MINUTES); // 合併指數成長與線性成長 const totalMultiplier = exponentialMultiplier + linearMultiplier; return Math.floor(initialBacteria * totalMultiplier); } 時間:60 秒TDD 實踐心得優點:✅ 快速反饋:每次只檢查最後幾行程式碼✅ 防止過度設計:只在測試驅動下加入邏輯✅ 累積測試套件:每個循環都留下可執行的規格✅ 信心倍增:重構時有測試保護循環節奏:每個循環 20-60 秒一小時可完成 30-60 個循環保持專注,立即獲得反饋📚 Part 3: 系統性測試設計方法1. 等價類劃分(Equivalence Partitioning)核心概念:將輸入範圍分成「行為相似」的區塊,每個區塊選一個代表值測試。輸入維度分析// 維度 1:minutesPassed(經過的分鐘數) 等價類 1:0 分鐘(初始狀態) 等價類 2:完整週期(20, 40, 60...) 等價類 3:不足一個週期(1-19 分鐘) 等價類 4:混合情況(完整週期 + 剩餘分鐘) // 維度 2:remainingMinutes(剩餘分鐘) 等價類 A:0 分鐘(剛好完整週期) 等價類 B:半個週期(10 分鐘,線性成長 50%) 等價類 C:其他分鐘數(需捨去小數) // 維度 3:initialBacteria(初始細菌數) 等價類 α:小數量(10, 50) 等價類 β:中等數量(100, 200) 等價類 γ:大數量(1000+) 測試案例選擇等價類代表值測試目的初始狀態(100, 0)驗證邊界條件1 個完整週期(100, 20)驗證指數成長2 個完整週期(100, 40)驗證多週期半個週期(100, 10)驗證線性近似混合場景(100, 30)驗證指數+線性2. 邊界值分析(Boundary Value Analysis)核心概念:錯誤最常發生在「邊界」,測試邊界值和邊界±1的值。BVA 測試策略對於範圍 [min, max],測試:最小值:min最小值+1:min + 1最大值-1:max - 1最大值:max應用到本案例// 週期邊界(20 分鐘) it('19 分鐘:週期前 1 分鐘', () => { expect(calculateBacteriaGrowth(100, 19)).toBe(195); // 100 × (1 + 19/20) = 195 }); it('20 分鐘:剛好完整週期', () => { expect(calculateBacteriaGrowth(100, 20)).toBe(200); }); it('21 分鐘:週期後 1 分鐘', () => { expect(calculateBacteriaGrowth(100, 21)).toBe(210); // 200 × (1 + 1/20) = 210 }); // 初始狀態邊界 it('0 分鐘:邊界', () => { expect(calculateBacteriaGrowth(100, 0)).toBe(100); }); it('1 分鐘:最小正整數', () => { expect(calculateBacteriaGrowth(100, 1)).toBe(105); // 100 × (1 + 1/20) = 105 }); 3. 錯誤猜測(Error Guessing)核心概念:基於經驗預測可能出錯的地方。潛在錯誤點// 錯誤 1:浮點數精度問題 // 3 分鐘:100 × (1 + 3/20) = 100 × 1.15 = 114.999... (可能) it('3 分鐘:應正確處理浮點數', () => { expect(calculateBacteriaGrowth(100, 3)).toBe(115); }); // 錯誤 2:整除問題 // 40 分鐘:剩餘分鐘應為 0,不應觸發線性計算 it('40 分鐘:2 個完整週期', () => { expect(calculateBacteriaGrowth(100, 40)).toBe(400); }); // 錯誤 3:指數運算錯誤 // 0 ** 0 在數學上有爭議,JavaScript 定義為 1 it('0 分鐘:驗證 2^0 = 1', () => { expect(calculateBacteriaGrowth(100, 0)).toBe(100); }); 測試覆蓋率矩陣測試類型涵蓋範圍測試數量重要性等價類劃分初始/完整週期/線性/混合4-5 個⭐⭐⭐⭐⭐邊界值分析0, 20, 40 分鐘邊界3-4 個⭐⭐⭐⭐浮點數處理Math.floor 正確性1-2 個⭐⭐⭐⭐不同初始值參數化測試2-3 個⭐⭐⭐完整測試套件// calculateBacteriaGrowth.test.js import { describe, it, expect } from 'vitest'; import { calculateBacteriaGrowth } from './calculateBacteriaGrowth.js'; describe('calculateBacteriaGrowth', () => { describe('邊界測試', () => { it('0 分鐘後,應回傳初始細菌數量', () => { expect(calculateBacteriaGrowth(100, 0)).toBe(100); }); }); describe('完整週期測試', () => { it('20 分鐘:1 個完整週期', () => { expect(calculateBacteriaGrowth(100, 20)).toBe(200); }); it('40 分鐘:2 個完整週期', () => { expect(calculateBacteriaGrowth(100, 40)).toBe(400); }); }); describe('線性成長測試', () => { it('10 分鐘:半個週期線性成長', () => { expect(calculateBacteriaGrowth(100, 10)).toBe(150); }); it('5 分鐘:1/4 週期線性成長', () => { expect(calculateBacteriaGrowth(100, 5)).toBe(125); }); }); describe('混合場景測試', () => { it('30 分鐘:1 週期 + 10 分鐘', () => { expect(calculateBacteriaGrowth(100, 30)).toBe(300); }); it('25 分鐘:1 週期 + 5 分鐘', () => { expect(calculateBacteriaGrowth(100, 25)).toBe(250); }); it('50 分鐘:2 週期 + 10 分鐘', () => { expect(calculateBacteriaGrowth(100, 50)).toBe(600); }); }); describe('浮點數精度測試', () => { it('3 分鐘:應無條件捨去浮點數', () => { expect(calculateBacteriaGrowth(100, 3)).toBe(115); }); it('7 分鐘:應無條件捨去浮點數', () => { expect(calculateBacteriaGrowth(100, 7)).toBe(135); }); }); }); // 執行:npx vitest // 結果:✅ PASS (9/9 tests) 🐛 Part 4: Number.EPSILON 浮點數精度問題問題發現執行測試時發現:// 測試 it('3 分鐘:應無條件捨去浮點數', () => { expect(calculateBacteriaGrowth(100, 3)).toBe(115); }); // 結果:❌ FAIL // Expected: 115 // Received: 114 Debug 過程// 加入 console.log 追蹤 export function calculateBacteriaGrowth(initialBacteria, minutesPassed) { const fullCycles = Math.floor(minutesPassed / 20); const exponentialMultiplier = 2 ** fullCycles; const remainingMinutes = minutesPassed % 20; const linearMultiplier = exponentialMultiplier * (remainingMinutes / 20); console.log('exponentialMultiplier:', exponentialMultiplier); // 1 console.log('linearMultiplier:', linearMultiplier); // 0.15 console.log('Sum:', exponentialMultiplier + linearMultiplier); // 1.15 const totalMultiplier = exponentialMultiplier + linearMultiplier; const result = initialBacteria * totalMultiplier; console.log('Before floor:', result); // 114.99999999999999 ← 問題! console.log('After floor:', Math.floor(result)); // 114 return Math.floor(result); } 根本原因:IEEE 754 浮點數表示JavaScript 使用雙精度浮點數(IEEE 754),某些十進制小數無法精確表示:// 預期 100 * 1.15 = 115.0 // 實際(IEEE 754) 100 * 1.15 = 114.99999999999999 // Math.floor 導致錯誤 Math.floor(114.99999999999999) = 114 // ❌ 原理:電腦使用二進制表示數字1.15 在二進制中是無限循環小數儲存時會產生微小誤差誤差累積導致 114.999... 而非 115.0解決方案:Number.EPSILONEPSILON 的定義根據 ECMAScript 規範和 MDN:Number.EPSILON 是「1 與大於 1 的最小浮點數之間的差值」 值為:2.220446049250313e-16修正程式碼const CYCLE_MINUTES = 20; const GROWTH_RATE = 2; export function calculateBacteriaGrowth(initialBacteria, minutesPassed) { const fullCycles = Math.floor(minutesPassed / CYCLE_MINUTES); const exponentialMultiplier = GROWTH_RATE ** fullCycles; const remainingMinutes = minutesPassed % CYCLE_MINUTES; const linearMultiplier = exponentialMultiplier * (remainingMinutes / CYCLE_MINUTES); const totalMultiplier = exponentialMultiplier + linearMultiplier; // ✅ 加入 Number.EPSILON 補償浮點數誤差 return Math.floor(initialBacteria * totalMultiplier + Number.EPSILON); } // 執行測試 // 結果:✅ PASS (9/9 tests) 工作原理// 修正前 114.99999999999999 + 0 = 114.99999999999999 Math.floor(114.99999999999999) = 114 // ❌ // 修正後 114.99999999999999 + 2.220446049250313e-16 ≈ 115.0 Math.floor(115.0) = 115 // ✅ 關鍵:EPSILON 足夠小,不會影響正確的計算,但足夠補償精度誤差。何時需要 Number.EPSILON?判斷框架(三個條件)需要 EPSILON 的充要條件: 1. 涉及浮點數運算(除法、乘法、特定小數)✅ 2. 使用 Math.floor/ceil/round 等整數化函數 ✅ 3. 運算結果可能接近整數邊界(例如 114.999... 接近 115)✅ 場景分析✅ 需要 EPSILON:// 場景 1:Math.floor 處理接近整數的浮點數 const price = 100; const taxRate = 1.15; Math.floor(price * taxRate + Number.EPSILON); // 115 ✅ // 場景 2:比較浮點數相等性 function areEqual(a, b) { return Math.abs(a - b) < Number.EPSILON; } areEqual(0.1 + 0.2, 0.3); // true ✅ // 場景 3:除法產生無限循環小數 const ratio = 3 / 20; // 0.15 Math.floor(100 * (1 + ratio) + Number.EPSILON); // 115 ✅ ❌ 不需要 EPSILON:// 場景 1:整數運算 const cycles = Math.floor(30 / 20); // 1(整數除法) // 場景 2:結果本身是浮點數(不做整數化) const average = (100 + 115) / 2; // 107.5(保持小數) // 場景 3:只有加減法的整數 const total = 100 + 50 + 25; // 175(整數加法) TDD vs. 預防性設計方案 A:預防性加入(保守)// 在所有 Math.floor 前都加 EPSILON const result = Math.floor(value + Number.EPSILON); 優點:✅ 一勞永逸,避免未來出錯✅ 程式碼明確表達「處理浮點數精度」的意圖缺點:⚠ 增加複雜度(如果運算本來就是整數)⚠ 可能隱藏真正的邏輯錯誤方案 B:TDD 驅動修復(推薦)✅// 先不加,讓測試發現問題 const result = Math.floor(value); // 測試失敗後,精確定位並修復 const result = Math.floor(value + Number.EPSILON); 優點:✅ YAGNI 原則:不過度設計✅ 測試案例記錄了問題場景✅ 只在真正需要的地方加入缺點:⚠ 依賴測試覆蓋率推薦策略:混合方法快速檢查表遇到 Math.floor/Math.ceil 時,問自己:□ 有浮點數運算嗎?(除法、乘法、特定小數) → 是 → 繼續檢查 → 否 → 不需要 EPSILON □ 結果可能接近整數嗎?(例如 114.999...) → 是 → 需要 EPSILON → 否 → 不需要 EPSILON □ 有測試覆蓋嗎? → 是 → 讓測試指引你 → 否 → 預防性加入 🏗️ Part 5: 架構設計 - Config-Driven Pattern背景:可配置的 CLI 工具假設我們要建立一個通用的 CLI 問答系統,可以透過配置文件定義問題和處理邏輯:// 使用範例 const questionConfigs = [ { name: "initialBacteria", prompt: "請輸入初始細菌數量:", validator: isPositiveInteger, }, { name: "minutesPassed", prompt: "請輸入經過分鐘數:", validator: isNonNegativeInteger, processor: ({ initialBacteria, minutesPassed }) => calculateBacteriaGrowth(initialBacteria, minutesPassed), }, ]; const result = await runQuestions(questionConfigs); console.log('結果:', result); 原始架構設計// processUserInputs.js export function processUserInputs(userInputs, questionConfigs) { const processedValues = {}; for (const config of questionConfigs) { const input = userInputs[config.name]; if (config.processor) { // ❌ 問題:用 config.name 作為結果的 key processedValues[config.name] = config.processor(userInputs); } else { processedValues[config.name] = input; } } return processedValues; } 發現的問題場景重現// Config { name: "minutesPassed", processor: ({ initialBacteria, minutesPassed }) => calculateBacteriaGrowth(initialBacteria, minutesPassed), // processor 返回:300(細菌數量) } // processUserInputs 執行後 processedValues.minutesPassed = 20; // 1. 先存入輸入值(20 分鐘) processedValues.minutesPassed = 300; // 2. 被 processor 覆蓋(300 隻細菌) // 問題:無法同時保留輸入和輸出 console.log(result.minutesPassed); // 300(細菌數) // 原本的 20 分鐘不見了! 實際影響// 期望的輸出 console.log(`經過 ${result.minutesPassed} 分鐘後,細菌數量為 ${result.numberOfBacteria} 隻`); // 實際輸出 console.log(`經過 ${result.minutesPassed} 分鐘後,細菌數量為 ${result.minutesPassed} 隻`); // → "經過 300 分鐘後,細菌數量為 300 隻" ❌ 語義錯誤! 解決方案:引入 resultKey架構改進// processUserInputs.js(改進版) export function processUserInputs(userInputs, questionConfigs) { const processedValues = {}; // 第一階段:收集所有輸入 for (const config of questionConfigs) { processedValues[config.name] = userInputs[config.name]; } // 第二階段:執行 processor for (const config of questionConfigs) { if (config.processor) { // ✅ 驗證:強制要求有 resultKey if (!config.resultKey) { throw new Error( `Config "${config.name}" has processor but missing resultKey.` ); } // ✅ 結果存到獨立欄位 processedValues[config.resultKey] = config.processor(processedValues); } } return processedValues; } 新的 Config 設計// bacteriaGrowthConfig.js export const questionConfigs = [ { name: "initialBacteria", prompt: "請輸入初始細菌數量:", validator: isPositiveInteger, }, { name: "minutesPassed", prompt: "請輸入經過分鐘數:", validator: isNonNegativeInteger, needsAllFields: true, // 標記需要多個輸入 resultKey: "numberOfBacteria", // ✅ 指定結果欄位名 processor: ({ initialBacteria, minutesPassed }) => calculateBacteriaGrowth(initialBacteria, minutesPassed), }, ]; 使用方式// main.js const result = await runQuestions(questionConfigs); // ✅ 輸入和輸出分離 console.log('輸入的初始細菌數:', result.initialBacteria); // 100 console.log('輸入的經過分鐘數:', result.minutesPassed); // 30 console.log('計算的細菌數量:', result.numberOfBacteria); // 300 // ✅ 語義正確的輸出 console.log(`經過 ${result.minutesPassed} 分鐘後,細菌數量約為 ${result.numberOfBacteria} 隻`); // → "經過 30 分鐘後,細菌數量約為 300 隻" ✅ 設計決策:語義化 vs. 通用化考慮過的方案方案 A:統一用 result(通用化)// Config resultKey: "result", // 所有題目都用 result // Main console.log('結果:', data.result); // 🤔 result 是什麼?細菌數?分數?金額? 優點:✅ 模板化程度高,複製貼上即可✅ 不用思考命名缺點:❌ 語義模糊:看到 result 不知道是什麼❌ Debug 時不友善❌ 複雜場景難擴展(如果需要多個結果?)方案 B:語義化命名(最終選擇)✅// Config resultKey: "numberOfBacteria", // 清楚表達業務意義 // Main console.log(`細菌數量:${data.numberOfBacteria} 隻`); // ✅ 一眼看出是細菌數量 優點:✅ 可讀性高,一眼看出是什麼結果✅ 符合 Clean Code 原則(自文件化)✅ Debug 友善✅ 易於擴展多結果場景缺點:⚠ 每題要思考命名(但只多花 10 秒)⚠ 模板不能完全統一(但可以模式化)最終決策// 推薦:使用語義化命名 // 範例 1:細菌實驗 resultKey: "numberOfBacteria" // 範例 2:成績計算 resultKey: "finalGrade" // 範例 3:價格計算 resultKey: "totalPrice" // 範例 4:比較結果 resultKey: "comparisonResult" 理由:✅ 六個月後回來看程式碼,立即理解✅ 別人接手時不用猜測✅ 複雜題目可以擴展(例如返回物件)✅ 符合「程式碼是寫給人看的」原則resultKey 的靈活性支援任何 JavaScript 類型:純值(Primitive)// Config { resultKey: "numberOfBacteria", processor: () => 300, // 返回 number } // 使用 console.log(result.numberOfBacteria); // 300 物件(Object)// Config { resultKey: "statistics", processor: ({ numbers }) => ({ average: calculateAverage(numbers), sum: calculateSum(numbers), max: Math.max(...numbers), min: Math.min(...numbers), }), } // 使用 console.log(result.statistics.average); // 50 console.log(result.statistics.sum); // 300 // 或解構 const { average, sum } = result.statistics; 陣列(Array)// Config { resultKey: "sortedNumbers", processor: ({ numbers }) => numbers.sort((a, b) => a - b), } // 使用 console.log(result.sortedNumbers); // [1, 2, 3, 4, 5] console.log(result.sortedNumbers[0]); // 1 架構優勢總結JavaScript 的動態特性// processUserInputs 不需要知道返回類型 processedValues[config.resultKey] = config.processor(...); // ↑ 這行程式碼適用於所有類型 優點:✅ 靈活:不限制返回類型✅ 簡單:不需要類型檢查✅ 擴展性強:未來可以返回更複雜的結構注意:⚠ 沒有類型安全(TypeScript 可以解決)⚠ 需要文件說明返回格式📖 關鍵技術知識總結1. JavaScript 語言特性指數運算子 **(ES2016)2 ** 3 = 8 2 ** 0 = 1 2 ** -1 = 0.5 // 優於 Math.pow Math.pow(2, 3) // 舊寫法 2 ** 3 // 新寫法(更簡潔) 餘數運算子 %25 % 20 = 5 // 取得除法餘數 20 % 20 = 0 // 剛好整除 19 % 20 = 19 // 小於除數 Math.floor()Math.floor(4.9) = 4 // 無條件捨去 Math.floor(-4.1) = -5 // 往負無窮方向捨去 Math.floor(114.999) = 114 // ⚠️ 注意浮點數精度 Number.EPSILONNumber.EPSILON = 2.220446049250313e-16 // 用途:補償浮點數精度誤差 Math.floor(114.99999999999999 + Number.EPSILON) = 115 // ✅ 2. 測試設計方法系統性測試流程1. 列出輸入維度 → initialBacteria, minutesPassed 2. 劃分等價類 → 初始/完整週期/線性/混合 3. 找出邊界值 → 0, 20, 40 分鐘 4. 錯誤猜測 → 浮點數、大數值 5. 優先級排序 → 核心邏輯 > 邊界 > 極端 6. 寫測試案例 → 從優先級 1 開始 測試覆蓋平衡3. TDD 實踐原則Uncle Bob 的建議循環時間:20-60 秒只寫剛好失敗的測試只寫剛好通過的程式碼重構改善設計,不改變行為Kent Beck 的 Commit 規範只在以下條件皆符合時才提交:所有測試皆通過 ✅所有編譯器或程式碼檢查警告皆已解決 ✅變更為單一邏輯工作單元 ✅採用「小而頻繁」的提交 ✅4. 架構設計原則職責分離// Config:定義結構和驗證規則 export const questionConfigs = [...] // Processor:處理業務邏輯 export function calculateBacteriaGrowth(...) {...} // Main:組合和輸出 async function main() {...} 語義化命名// ✅ 好命名 resultKey: "numberOfBacteria" resultKey: "finalScore" resultKey: "totalPrice" // ❌ 差命名 resultKey: "result" // 太通用 resultKey: "num" // 縮寫 resultKey: "data" // 模糊 防呆設計// 強制驗證:有 processor 必須有 resultKey if (config.processor && !config.resultKey) { throw new Error(`Missing resultKey for config "${config.name}"`); } 🏆 總結與反思今日學習成果技術知識:✅ TDD 超小步推進實戰(9 個測試案例)✅ 系統性測試設計方法(等價類、邊界值、錯誤猜測)✅ JavaScript 浮點數精度問題與 Number.EPSILON✅ Config-Driven 架構設計與重構軟技能:✅ 問題分解能力(將複雜需求拆成小循環)✅ Debug 思維(從測試失敗追蹤到根本原因)✅ 設計決策能力(語義化 vs. 通用化的權衡)✅ 重構勇氣(發現架構缺陷後果斷改進)關鍵啟發1. TDD 的真正價值不是「先寫測試」,而是「讓測試引導設計」每個測試都是一個需求規格測試失敗指出下一步要做什麼重構時測試提供安全網累積的測試套件就是活文件2. 浮點數問題的普遍性JavaScript 的浮點數不是 Bug,是特性IEEE 754 是標準,不是 JavaScript 特有不只 JavaScript,所有語言都有此問題關鍵是「知道何時需要補償」用 TDD 發現問題比預防性設計更精準3. 架構設計的演進好的架構是演進出來的,不是一次設計對的先滿足需求(MVP)遇到問題時重構(Pain-Driven Refactoring)每次重構都讓系統更好一點測試保證重構不破壞功能📚 參考資源文章與文件Uncle Bob - The Cycles of TDDMDN - Number.EPSILONMDN - Math.floor()ECMAScript 規範 - Number.EPSILON測試方法Boundary Value Analysis - GeeksforGeeksEquivalence Partitioning - Zen8LabsTest Case Design Techniques - Lotus QA工具與框架Vitest - 快速單元測試框架IEEE 754 標準說明123 ## Publication Information - [雞蛋糕的前端修煉屋](https://paragraph.com/@gcake/): Publication homepage - [All Posts](https://paragraph.com/@gcake/): More posts from this publication - [RSS Feed](https://api.paragraph.com/blogs/rss/@gcake): Subscribe to updates