# Day 10:測試驅動開發 (TDD) 的原則與紀律 **Published by:** [雞蛋糕的前端修煉屋](https://paragraph.com/@gcake/) **Published on:** 2025-12-03 **URL:** https://paragraph.com/@gcake/day-10 ## Content 複習與更新:JS 程式碼執行整體流程 JavaScript Visualized - Promise Execution JavaScript Visualized - Event Loop, Web APIs, (Micro)task Queue Promise影片版: JavaScript Visualized: Promise Execution 文字版: JavaScript Visualized: Promise ExecutionPromise 執行順序重點Constructor 是同步的;executor 會立刻進入 Call Stack 執行。then/catch/finally 的回呼排入 Microtask Queue;setTimeout 等 Web APIs 的回呼排入 Task Queue(Macro task)。Event Loop 順序:清空 Call Stack → 取出所有 Microtasks → 取出一個 Task → 可能觸發渲染再循環。Event Loop影片版: JavaScript Visualized - Event Loop, Web APIs, (Micro)task Queue 文字版: JavaScript Visualized - Event Loop, Web APIs, (Micro)task QueueWeb APIscallback basedpromise basedClean Craftsmanship 的影片示範實作練習題Uncle Bob 為這本書的內容錄製了幾支示範影片,台灣讀者在博碩出版商網站可以下載影片檔、含時間戳的 txt 檔和第五章的範例程式碼 txt 檔(方便直接複製到 IDE 閱讀和執行體驗該章「重構」的內容)。 第一支影片用 Java 實作一個 stack 示範實作「在幾秒鐘內完成紅燈→綠燈→重構」循環。 照著書中建議先看影片再看文字說明,深刻感受到看影片體驗到的節奏感真的不是文字描述可以表達的。也發現我前面幾天自己看文件理解練習的 TDD 是「邏輯對了,實作流程不太對」的版本:我把一題裡面會用到的 function 的測試全部寫完(包含定義、輸入、預期輸出、例外),才去寫產品程式碼中的 function這樣整塊整塊做確實減少很多切換視窗的麻煩,但沒有完整跑過「編譯錯誤→測試行為→清理程式碼/重構」的思考邏輯。 我現在的理解是:TDD 這個方法核心概念是要把寫程式碼的思考和實作步驟拆到幾行以內,確保跨出每一個小步都是正確合理的,只要通過測試的程式碼就是可以安全部署 (deploy) 的程式碼。也企圖以此儘可能避免「一個看起來可以動的程式碼」後續要大量 debug 的工作。 今天的 TDD 練習: 以 JavaScript function 完成整數 queue 的 circular buffer,物件導向的寫法之後再說😆 寫起來還是滿卡的,思路不是很順,要習慣一下每次只推進幾行的思考方式整數堆疊書中題目敘述: 按照示範的方式實作一個先進先出的整數堆疊 (queue) 。請使用固定大小的陣列來存放整數。你可能會需要兩個指標 (pointer) 來追蹤元素的新增位置和移除位置。完成後,你會發現自己實作了一個循環緩衝區 (circular buffer)。實作架構選擇資料結構(Plain Object):{ buffer: [], // 固定大小陣列 capacity: 3, // 容量上限 headIndex: 0, // 讀取位置 tailIndex: 0, // 寫入位置 size: 0 // 當前元素數量 } 繞圈(Wrap Around)核心:以模運算限制索引在 0..capacity-1tailIndex = (tailIndex + 1) % capacityheadIndex = (headIndex + 1) % capacity JavaScript 的餘數運算子 % 可用於此,確保索引不會超出陣列邊界並能回到 0。TDD 實作紀錄:紅燈、綠燈、重構Step 1: 建立空 Queue測試目標:size === 0、isEmpty(queue) === true、isFull(queue) === false。語言特性參考:陣列與物件屬性皆為標準內建類型與結構。Step 2: 單一 Enqueue / Dequeue (Happy Path)測試目標:enqueue 後 size === 1 且 peek(queue) 為該值;dequeue 能拿回該值並回到空。Debug 經驗:ReferenceError: peak is not defined → 函式命名誤用(應為 peek)。取得 undefined → 少了真正的賦值動作(queue.buffer[queue.tailIndex] = value),屬於語法疏漏由測試攔截。Step 3: 邊界條件 (Edge Cases)isFull(queue) 當 size === capacity 時為真;isEmpty(queue) 當 size === 0 時為真。空取值/滿插入應拋出錯誤(queue is empty / queue is full),測試用 toThrow 斷言此行為。Step 4: 整合測試 - 繞圈行為 (Circular Buffer Logic)測試描述:在 buffer 內多次反覆填值和取值,部分取出與再填入的次數不規則時,仍能按照先進先出的正確順序取值。驗證重點:tailIndex / headIndex 超過末端後能回到 index 0 繼續運作,取出順序保持 FIFO。Vitest:describe / it / expect 常用法每個 it 皆建立自己的 queue 實例,避免測試間狀態汙染;必要時使用 beforeEach 於每次測試前建立新實例。函式版 API 時需呼叫函式判斷狀態:isEmpty(queue) / isFull(queue);非 queue.isEmpty / queue.isFull(除非以 class getter 設計)。常見斷言:等值:expect(value).toBe(expected)。丟錯:expect(() => fn()).toThrow('message')。參考文件:Vitest API 參考(describe/it/expect/toThrow 等):https://vitest.dev/api/測試常見錯誤與隔離原則:Vitest Common Errors / Environment。JS 語言與資料結構文件MDN JavaScript 指南與資料結構(物件、陣列、型別):MDN JS / 資料型別與資料結構 / Array 參考餘數(%)運算子(繞圈核心運算):Remainder (%) - MDN附錄完整測試程式碼(Vitest)// int-queue.test.js import { describe, expect, it } from 'vitest'; import { createIntQueue, isEmpty, isFull, enqueue, dequeue, peek, } from './int-queue.js'; describe(`IntQueue`, ()=>{ it(`create an empty queue`, ()=>{ const queue = createIntQueue(3); expect(queue.size).toBe(0); expect(isEmpty(queue)).toBe(true); expect(isFull(queue)).toBe(false) }) }) it(`enqueues and dequeues one value`,()=>{ const queue = createIntQueue(3); enqueue(queue, 42); expect(queue.size).toBe(1) expect(isEmpty(queue)).toBe(false) expect(peek(queue)).toBe(42) const value = dequeue(queue); expect(value).toBe(42) expect(queue.size).toBe(0) expect(isEmpty(queue)).toBe(true) }) it('becomes full after enqueuing capacity times',()=>{ const queue = createIntQueue(3); expect(isFull(queue)).toBe(false); enqueue(queue, 1); enqueue(queue, 2); enqueue(queue, 3); expect(isFull(queue)).toBe(true); expect(queue.size).toBe(3); }) it('throws error when enqueue to a full queue',()=>{ const queue = createIntQueue(3); enqueue(queue, 1); enqueue(queue, 2); enqueue(queue, 3); expect(() => enqueue(queue, 4)).toThrowError('queue is full'); }); it('throws error when dequeue from an empty queue',()=>{ const queue = createIntQueue(3); expect(() => dequeue(queue)).toThrowError('queue is empty'); }); it('在多次不規則的新增與取出後,索引繞圈時仍維持先進先出的正確順序', () => { const queue = createIntQueue(3) // 初次填入兩個 enqueue(queue, 1) enqueue(queue, 2) expect(dequeue(queue)).toBe(1) // head: 0 -> 1,取出 1 expect(peek(queue)).toBe(2) // 目前隊首是 2 // 填滿剩下的空間,讓 tail 繞回 index 0 enqueue(queue, 3) // buffer: [ , 2, 3],head = 1, tail = 0 enqueue(queue, 4) // buffer: [4, 2, 3],head = 1, tail = 1 (繞回到 0 再往前) // 依序取出,確認順序仍然正確 expect(dequeue(queue)).toBe(2) // head: 1 -> 2,取出 2 expect(dequeue(queue)).toBe(3) // head: 2 -> 0,取出 3(head 繞回 0) expect(dequeue(queue)).toBe(4) // head: 0 -> 1,取出 4 expect(isEmpty(queue)).toBe(true) }) 完整功能程式碼// int-queue.js export function createIntQueue(capacity){ if(!Number.isInteger(capacity)||capacity<=0){ throw new Error('capacity must be a positive integer') } return {buffer:new Array(capacity), capacity, headIndex:0, tailIndex:0, size:0 } } export function isEmpty(queue){ return queue.size === 0; } export function isFull(queue){ return queue.size === queue.capacity; } export function enqueue(queue, value){ if(!Number.isInteger(value)){ throw new Error('value must be an integer'); } if (isFull(queue)){ throw new Error('queue is full'); } queue.buffer[queue.tailIndex]=value; queue.tailIndex = (queue.tailIndex + 1) % queue.capacity; queue.size += 1; } export function peek(queue){ if (isEmpty(queue)){ throw new Error('queue is empty'); } return queue.buffer[queue.headIndex]; } export function dequeue(queue){ if(isEmpty(queue)){ throw new Error('queue is empty'); } const value = queue.buffer[queue.headIndex]; queue.buffer[queue.headIndex]=undefined; queue.headIndex=(queue.headIndex + 1) % queue.capacity; queue.size -= 1; return value; } ## 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