# Day 16:規格化測試實作、以宣告式寫法重構命令式、DOM 與事件流 **Published by:** [雞蛋糕的前端修煉屋](https://paragraph.com/@gcake/) **Published on:** 2025-12-11 **URL:** https://paragraph.com/@gcake/day-16 ## Content 今天主要完成兩塊:一是「單元測試的層級該怎麼切才好讀?」,二是「如何把充滿 for 迴圈的命令式程式碼,優化成乾淨的宣告式邏輯」,並整理 DOM/BOM 與事件流的閱讀筆記。一、測試規格的選擇:什麼時候該用巢狀 describe?在使用 Vitest 或 Jest 撰寫測試時,我們常會糾結:到底要寫幾層 describe?是越平展越好,還是層層包裹比較清晰? 核心原則其實很簡單:測試報告是寫給人看的規格書。目標不是為了分類而分類,而是為了讓閱讀報告的人能一眼看出「現在壞掉的是哪個情境」。推薦使用巢狀 describe 的情境區分不同面向 當同一個函式有多種檢查維度時。例如一個字串計算函式:describe('字串計算器', () => { describe('計算正確性', () => { /* 驗證加減乘除結果 */ }); describe('輸入邊界檢查', () => { /* 驗證非法輸入、空值 */ }); describe('輸出格式', () => { /* 驗證小數點位數、字串模板 */ }); }); 這樣測試報告會呈現:字串計算器 › 輸出格式 › 應保留兩位小數,路徑非常清晰。區分不同狀態(Context)當函式行為依賴外部狀態時:describe('UserService', () => { describe('使用者已登入', () => { /* 測試權限與資料存取 */ }); describe('使用者未登入', () => { /* 測試轉導與錯誤訊息 */ }); }); 隔離 beforeEach 的作用域這是一個很實用的技巧。如果你只希望某幾組測試共用一個 setup(例如建立一個含有資料的清單),可以透過巢狀 describe 把 beforeEach 鎖在該層級內,避免汙染全域測試環境。什麼時候不需要巢狀 describe?如果你的測試只是針對「同一個邏輯」進行「多組資料窮舉」,請不要再多包一層 describe('多組輸入')。直接使用參數化測試(Parameterized Testing)會更簡潔:// ✅ 推薦:直接用 test.each describe('加法運算', () => { test.each([ [1, 2, 3], [5, 5, 10], [-1, 1, 0] ])('%i + %i 應該等於 %i', (a, b, expected) => { expect(add(a, b)).toBe(expected); }); }); 小結判斷標準:這一群測試講的是「不同的規格/情境」嗎?是 → 包 describe。這一群測試只是「不同的輸入資料」嗎?是 → 用 test.each。二、實戰重構:從命令式 (Imperative) 到宣告式 (Declarative)今天處理了一個經典的資料處理需求:「在指定範圍內找出符合條件的數字(如質數),並計算其衍生的數值(如倍數)」。第一階段:命令式寫法 (The Naive Way)一開始為了快速實作,很容易寫出這種充滿 for 迴圈與 if 判斷的程式碼:// ❌ 充滿細節控制,閱讀時需要腦內編譯 function getRangeData(lower, upper) { const result = []; for (let i = lower; i <= upper; i++) { if (isPrime(i)) { // 甚至可能在這裡又寫了一層 for loop 找倍數 result.push(i); } } return result; } 這種寫法的缺點在於我們必須手動管理索引(index)、邊界條件以及暫存陣列(temp array),容易出錯且難以閱讀。第二階段:宣告式重構 (The Refactored Way)透過 TDD(測試驅動開發)先建立了保護網後,我將上述邏輯重構。首先,抽出一個通用的工具函式來解決「產生範圍」的問題:// 透過 Array.from 產生連續數列 function createRange(lower, upper) { const length = upper - lower + 1; return Array.from({ length }, (_, index) => lower + index); } 接著,主邏輯就可以像寫文章一樣順暢:// ✅ 關注「我要什麼」而不是「怎麼做」 function getRangeData(lower, upper) { return createRange(lower, upper) .filter(isPrime) // 1. 篩選質數 .map(num => ({ // 2. 轉換格式 prime: num, multipliers: findMultipliers(num) })); } 重構心得:createRange 的價值:把重複的 for 迴圈邏輯抽離後,主程式碼瞬間乾淨了。Array Method Chaining:善用 filter 接 map,能讓資料流向一目了然(Input → Filter → Transform → Output)。TDD 的安全感:因為先寫了邊界測試(例如輸入範圍不存在質數時回傳空陣列),重構過程中我完全不擔心改壞功能。三、JavaScript 基礎複習:DOM 與事件流除了邏輯訓練,今天也重新整理了瀏覽器運作機制的筆記。1. 關注點分離 (Separation of Concerns)前端三要素各司其職:HTML (結構):負責資料語意。CSS (樣式):負責視覺呈現。JavaScript (行為):負責互動邏輯。原則上,樣式變更應盡量透過 JS 切換 class 來達成,而非直接操作 style 屬性,這樣才能保持 CSS 的可維護性。2. 事件流 (Event Flow)from W3C 瀏覽器的事件傳遞並非單向,而是分為三個階段:捕捉階段 (Capturing):事件由 window 往下傳遞至目標元素。目標階段 (Target):事件到達實際點擊的元素。冒泡階段 (Bubbling):事件由目標元素往上回傳至 window。實戰技巧:event.stopPropagation():這是控制事件流的關鍵,可以用來阻止事件繼續往上冒泡或往下捕捉。在處理複雜的巢狀 UI(如 Modal 內的點擊事件)時非常重要。window vs global:在瀏覽器中,全域變數 var 會掛載在 window 上;但在 Node.js 環境中,全域物件是 global 且行為稍有不同。這是在跨環境開發時需注意的細節。總結今天的練習讓我深刻體會到,寫程式除了要把功能做出來,更要注意設計程式碼的結構。無論是測試案例的分組,還是資料處理流程的宣告式寫法,目的都是為了降低未來的維護成本與認知負擔。參考文件Array.from – MDNArray.prototype.filter – MDNArray.prototype.map – MDNArray – MDNVitest API ReferenceVitest GuideDOM events – MDNEvent – MDNEvent: stopPropagation – MDNBubbling and capturing ## 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