# Day 54 verifyPassword 的第一個 Vitest 測試(續);CSS 留白與定位屬性 **Published by:** [雞蛋糕的前端修煉屋](https://paragraph.com/@gcake/) **Published on:** 2026-02-05 **URL:** https://paragraph.com/@gcake/day-54 ## Content 今日閱讀:《單元測試的藝術》3/e 2.9 p.65 - 2.11 p.71 第二章完參數化的測試程式測試相同的情境,但工作單元的輸入有一些小變化時使用 先前練習題已經做過,略讀 Act API:test.each([]) 用陣列封裝輸入的參數,Vitest 會自動映射到對應位置,對每個陣列執行一次 [ parameter, asserertion ]完成相同情境不同輸入值的測試 使用JS 原生 for loop 也可以做到,但要考慮可讀性 另外,使用參數化測試要注意是否把不同條件包在一起測試了,以書中範例為例,輸入全小寫字母與一個大寫字母就應該被視為不同情境,拆開測試檢查預期會被拋出的錯誤先前練習題已經做過,略讀 Assersion API:expect().toThrowError() 作者附註的快照測試 API 也練習過用來比對 ASCII 圖形處理結果toMatchInlineSnapshot測試分類一、Vitest 測試分類功能對照Jest vs Vitest 對照表功能JestVitest差異說明檔案路徑過濾--testPathPattern直接傳入檔案名稱Vitest 簡化語法 vitest測試名稱過濾-t, --testNamePattern-t, --testNamePattern完全相同 vitest配置檔案jest.config.jsvitest.config.js格式類似 vitest指定測試模式testMatchinclude語意相同 lutece.github排除測試testPathIgnorePatternsexclude語意相同 lutece.github多專案配置-projectsVitest 獨有 vitest測試標籤---tags-filterVitest 4.1+ 獨有 main.vitestCLI 指令範例# 檔案路徑過濾 vitest basic # 執行包含 "basic" 的測試檔案 vitest auth login # 執行包含 "auth" 和 "login" 的檔案 # 測試名稱過濾 vitest -t "user login" # 根據測試名稱過濾 vitest --testNamePattern="auth.*" # 使用正則表達式 # 指定檔案與行號(Vitest 3+) vitest basic/foo.test.ts:10 # 執行特定檔案的特定行測試 配置檔案:使用 Projects 分類// vitest.config.js import { defineConfig } from 'vitest/config' export default defineConfig({ test: { // 基本配置 include: ['**/*.{test,spec}.{js,ts}'], exclude: ['node_modules', 'dist', '**/*.e2e.test.js'], // 多專案配置 projects: [ { // 單元測試專案 test: { name: 'unit', include: ['**/*.unit.test.ts'], isolate: false, // 提升速度 }, }, { // 整合測試專案 test: { name: 'integration', include: ['**/*.integration.test.ts'], testTimeout: 30000, // 較長超時 }, }, ], }, }) package.json 腳本設定{ "scripts": { "test": "vitest --project=unit", // 開發時持續執行 "test:integration": "vitest --project=integration", "test:all": "vitest", // CI/CD 完整測試 "test:auth": "vitest auth", // 測試特定功能模組 "precommit": "vitest run --project=unit --changed" // Git hook } } 二、為什麼需要測試分類?1. 執行速度差異核心差異單元測試:隔離測試個別元件,純邏輯運算,執行時間 < 10ms repeato整合測試:驗證多元件協作,涉及外部資源(資料庫、網路、檔案系統),執行時間 > 500ms stackoverflow// 單元測試:快速(< 10ms) describe('calculateDiscount', () => { it('應計算九折優惠', () => { expect(calculateDiscount(100, 0.9)).toBe(90); }); }); // 整合測試:較慢(> 500ms) describe('UserRepository', () => { it('應從資料庫讀取使用者資料', async () => { const user = await userRepo.findById(123); expect(user.name).toBe('John'); }); }); 測試套件膨脹問題 專案成長後測試數量可達數百至數千個: repeato不分類:全部測試可能需要 30 分鐘以上分類後:單元測試 2-3 分鐘,整合測試按需執行2. TDD 紅綠重構循環需求幾秒鐘完成一輪循環 TDD 講究「紅燈 → 綠燈 → 重構」的快速迭代,理想循環時間為數秒鐘 。如果測試套件包含慢速整合測試,會打斷開發心流,失去即時回饋優勢。 itential 開發節奏對比有測試分類:改代碼 → 2 秒後看到單元測試結果 → 繼續開發無測試分類:改代碼 → 等待 5 分鐘全部測試跑完 → 心流中斷3. 測試穩定性與維護成本隔離測試的穩定性單元測試:不受外部環境影響,測試結果穩定可靠 vocus整合測試:可能因網路波動、資料庫狀態、第三方 API 變更產生「片狀測試」(flaky test),需額外維護 stackoverflow測試失敗的診斷效率單元測試失敗:精準定位到特定函式問題整合測試失敗:需檢查多個元件互動,診斷時間較長 codefresh4. 測試演化與重構單元測試可能變成整合測試 隨著重構,原本的單元測試可能演變成整合測試: stackoverflow// 最初:單一類別的單元測試 describe('OrderProcessor', () => { it('應計算訂單總金額', () => { const processor = new OrderProcessor(); expect(processor.calculateTotal([{price: 100}])).toBe(100); }); }); // 重構後:多個類別協作的整合測試 describe('OrderService', () => { it('應處理完整訂單流程', () => { // 現在涉及 OrderProcessor、PaymentGateway、InventoryService const result = orderService.processOrder(orderData); expect(result.status).toBe('success'); }); }); 此時應為新拆分的類別撰寫新的單元測試,並將原測試重新分類為整合測試。判斷整合測試的標準符合以下任一條件即為整合測試: ithelp.ithome.com與外部環境互動(資料庫、檔案系統、網路、時間)需要多個真實元件協作執行時間明顯較長三、測試分類對 CI/CD 的好處1. 快速回饋循環階段性品質閘門 設計多層測試閘門,越快的測試越早執行: codefresh# .github/workflows/ci.yml name: CI Pipeline on: [push, pull_request] jobs: # 第一階段:單元測試(1-3 分鐘) unit-tests: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - run: npm install - run: npm run test:unit # 第二階段:整合測試(5-10 分鐘) integration-tests: needs: unit-tests # 單元測試通過才執行 runs-on: ubuntu-latest steps: - run: npm run test:integration # 第三階段:E2E 測試(15-30 分鐘) e2e-tests: needs: integration-tests runs-on: ubuntu-latest steps: - run: npm run test:e2e 即時問題定位單元測試 2 分鐘內失敗 → 記憶猶新時修正,成本低混合測試 30 分鐘後失敗 → 已切換任務,修復成本高 provar2. 資源使用效率快速失敗策略 CI/CD 平台通常按運算時數計費,測試分類可避免浪費: 33rdsquare// vitest.config.js - 設定失敗即停止 export default defineConfig({ test: { bail: 1, // 第一個失敗就停止 }, }) 效益範例單元測試失敗:2 分鐘 × 成本 → 立即停止不分類全跑:30 分鐘 × 成本 → 浪費 15 倍資源 codefresh3. 並行執行最佳化Vitest Sharding 分片執行# CI 環境分配到 3 台機器 # Machine 1 vitest --project=unit --shard=1/3 --reporter=blob # Machine 2 vitest --project=unit --shard=2/3 --reporter=blob # Machine 3 vitest --project=unit --shard=3/3 --reporter=blob vitest --project=integration --reporter=blob # 最後合併報告 vitest --merge-reports --reporter=junit 時間節省計算 假設測試套件:單元測試:300 個(共 10 分鐘)整合測試:50 個(共 20 分鐘)執行策略時間不分類順序執行30 分鐘分類並行執行23.3 分鐘智慧分配(單元與整合同時跑)20 分鐘 33rdsquare4. 不同觸發情境的測試策略精準匹配測試範圍觸發情境執行測試時間目的本地提交前單元測試1-2 分鐘快速驗證邏輯正確性 dzonePush 到分支單元 + 整合5-15 分鐘確保元件協作正常 codefreshPull Request全部測試30-60 分鐘完整品質驗證 codefresh定時任務(每晚)全部 + 效能測試數小時深度回歸測試 provar// package.json - 不同場景的腳本 { "scripts": { "precommit": "vitest run --project=unit --changed", "test:ci-push": "vitest run --project=unit --project=integration", "test:ci-pr": "vitest run", "test:nightly": "vitest run && npm run test:e2e && npm run test:perf" } } 5. 測試穩定性管理針對不同測試類型設定重試策略// vitest.config.js export default defineConfig({ test: { projects: [ { name: 'unit', include: ['**/*.unit.test.ts'], retry: 0, // 單元測試不重試,失敗即有問題 }, { name: 'integration', include: ['**/*.integration.test.ts'], retry: 2, // 整合測試允許重試 2 次 testTimeout: 30000, // 較長超時時間 }, ], }, }) 穩定性監控目標單元測試成功率:應維持 99.9%+ katalon整合測試成功率:目標 95%+,需持續優化片狀測試6. 不同環境的測試配置環境隔離策略# CI 配置範例 jobs: unit-tests: runs-on: ubuntu-latest # 輕量環境 services: {} # 不需要額外服務 integration-tests: runs-on: ubuntu-latest services: postgres: # 需要資料庫 image: postgres:14 redis: # 需要快取服務 image: redis:7 成本控管策略 codefresh單元測試:使用免費 CI runner整合測試:使用付費 runner 但限制並行數量E2E 測試:僅在 PR 與 merge 時執行四、實務建議檔案命名慣例用檔案名稱區分測試類型,方便 Vitest 過濾: vitestsrc/ ├── utils/ │ ├── math.ts │ ├── math.unit.test.ts // 單元測試 │ └── math.integration.test.ts // 整合測試 ├── services/ │ ├── orderService.ts │ ├── orderService.unit.test.ts │ └── orderService.integration.test.ts 測試金字塔原則維持「70% 單元測試 + 20% 整合測試 + 10% E2E 測試」的比例,確保: codefresh大部分問題在單元測試階段(1-2 分鐘)被攔截整合測試驗證關鍵互動(5-10 分鐘)E2E 測試保障使用者情境(15-30 分鐘)監控 CI 管道效能定期檢視測試執行時間趨勢: octopus單元測試平均時間 < 5 分鐘Pull Request 完整測試 < 15 分鐘主分支全量測試 < 30 分鐘當測試時間持續增長時:檢查是否有測試分類錯誤(整合測試誤放在單元測試)啟用並行執行與分片功能移除冗餘或重複的測試案例測試覆蓋率分層追蹤針對不同測試類型設定合理覆蓋率目標:// vitest.config.js export default defineConfig({ test: { projects: [ { name: 'unit', coverage: { lines: 90 }, // 單元測試應覆蓋大部分邏輯 }, { name: 'integration', coverage: { lines: 60 }, // 整合測試聚焦關鍵路徑 }, ], }, }) 五、核心結論測試分類是現代軟體工程的必備實踐,讓 CI/CD 流程從「全有全無」變成「漸進式驗證」: getscandium開發體驗:TDD 紅綠重構保持數秒鐘快速循環品質保證:多層閘門確保不同層級的問題都能被攔截成本效益:避免浪費運算資源,快速失敗節省時間與金錢維護性:穩定的單元測試 + 可控的整合測試,降低長期維護成本Vitest 提供比 Jest 更現代化的測試分類功能(projects、tags、sharding),讓測試組織更靈活彈性,是前端工程師值得深入學習的工具。參考資料Vitest 官方文件 - Test FilteringVitest 官方文件 - Test ProjectsCI/CD Testing Best PracticesCSS 留白與定位屬性學習目標:理解傳統 CSS(不用 Flex/Grid)如何用留白 + 定位打造精準佈局一、對話中提到的所有 CSS 屬性清單1.1 盒模型留白屬性屬性作用範圍特性margin元素與元素之間不會有背景色,可以用負值,會發生合併(collapse)padding元素內容到邊框會保留背景色,撐大可點擊範圍,影響總寬度border元素邊框佔據空間,可設定顏色/樣式/粗細gapFlex/Grid 子元素間距只作用於現代佈局,不會在容器邊緣產生空隙1.2 文字留白屬性屬性作用對象使用情境line-height文字行高段落可讀性,單行置中,用純數字最佳(如 1.6)letter-spacing字元間距標題視覺效果,中英文混排,Logo 設計word-spacing單詞間距英文排版(對中文無效)text-indent段落首行縮排傳統文章排版white-space空白字符處理控制換行與空白顯示1.3 盒模型計算屬性屬性計算邏輯推薦值box-sizing: content-boxwidth 只算 content,padding/border 會額外增加預設值(不推薦)box-sizing: border-boxwidth = content + padding + border,往內推擠全站設定(推薦)1.4 定位屬性屬性定位基準是否脫離文件流position: static預設,按文件流排列否position: relative自己的原始位置否(保留原空間)position: absolute最近的已定位父元素是(不佔空間)position: fixed瀏覽器視窗是(不佔空間)1.5 定位座標屬性屬性作用搭配對象top / bottom / left / right指定元素距離定位基準的距離position: relative/absolute/fixedtransform: translate()相對自身尺寸位移常與 position: absolute 搭配置中1.6 其他相關屬性屬性作用使用情境display: table-cell假裝元素是表格儲存格搭配 vertical-align: middle 垂直置中vertical-align行內元素垂直對齊只對 inline/inline-block/table-cell 有效二、定位屬性的核心概念2.1 position: relative(相對定位) steam.oxxostudio比喻:像在地上畫記號,告訴物件「從你原本的位置移動」。 steam.oxxostudio.box { position: relative; top: 20px; /* 從原位置往下移 20px */ left: 30px; /* 從原位置往右移 30px */ } 特性: web保留原空間:元素移走後,原位置仍保留空位(其他元素不會遞補)不影響其他元素:位移只是視覺效果,不會推擠鄰居常用於建立定位參考點:讓子元素的 absolute 以它為基準 vocus2.2 position: absolute(絕對定位) eudora比喻:像飛出文件流的氣球,可以飄到任何地方,不佔地面空間。 vocus.parent { position: relative; /* 建立參考點 */ } .child { position: absolute; top: 0; /* 距離父元素上緣 0px */ right: 0; /* 距離父元素右緣 0px */ } 定位基準規則: eudora有定位的父元素:找最近的 position: relative/absolute/fixed 的父元素沒有定位的父元素:以 或初始包含區塊為基準 vocus特性: eudora脫離文件流:不佔原本空間,其他元素會遞補上來不推擠其他元素:怎麼移動都不影響鄰居排列寬高自動收縮:沒指定寬高時,會縮到內容大小 eudora三、留白屬性 × 定位屬性的搭配技巧3.1 margin 與 top/left 的差異 code2study.blogspot當元素使用 position: relative 時:屬性組合效果適用情境margin-top: 20px推擠其他元素,會影響佈局流需要調整整體佈局top: 20px只移動自己,不影響其他元素微調單一元素位置/* 範例對比 */ .method-1 { position: relative; margin-top: 20px; /* 會把下方所有元素往下推 */ } .method-2 { position: relative; top: 20px; /* 只有自己往下移,保留原空間 */ } 當元素使用 position: absolute 時: code2study.blogspotmargin 和 top/left 效果相同,因為元素已脫離文件流推薦用 top/left:語意更清楚,表示「定位座標」3.2 padding 在 absolute 元素中的應用 zero-plus.parent { position: relative; padding: 50px; /* 父元素的 padding 會影響子元素的定位起點 */ border: 20px solid orange; } .child { position: absolute; top: 0; /* 起點是父元素 padding 內緣,不是 border 內緣 */ left: 0; width: 100px; height: 100px; } 關鍵規則: zero-plusabsolute 子元素的定位起點 = 父元素的 padding 內緣父元素的 padding 會「內推」子元素的 (0, 0) 起點父元素的 margin 對子元素定位無影響3.3 margin: auto 置中法(需搭配 absolute) realnewbie.outer { position: relative; height: 300px; } .inner { position: absolute; top: 0; bottom: 0; /* 上下都設為 0 */ left: 0; right: 0; /* 左右都設為 0 */ margin: auto; /* 瀏覽器會自動計算剩餘空間 */ width: 200px; /* 必須有固定寬高 */ height: 100px; } 運作原理: realnewbie同時設定 top: 0; bottom: 0; 會產生「垂直方向的拉扯」margin: auto 會平均分配上下剩餘空間水平方向同理限制:子元素必須有明確的寬高,不適合響應式內容。 realnewbie3.4 transform 位移不佔空間.box { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); /* 百分比相對自身尺寸 */ } 關鍵差異: pjchender.githubtop: 50%:相對父元素高度的 50%transform: translateY(-50%):相對自身高度的 50%transform 不會改變元素佔據的空間,只是視覺位移四、實戰案例:留白 + 定位組合拳4.1 卡片角標設計.card { position: relative; /* 建立定位參考點 */ padding: 20px; /* 內容留白 */ border: 1px solid #ddd; margin-bottom: 16px; /* 卡片間距 */ } .badge { position: absolute; top: -10px; /* 從卡片上緣往上偏移(負值讓它跑出去) */ right: 10px; /* 從卡片右緣往左 10px */ padding: 5px 10px; /* 角標內部留白 */ background: red; } 留白作用分析:card 的 padding:保護內文不貼邊card 的 margin-bottom:卡片之間保持距離badge 的 padding:讓文字不貼齊角標邊緣badge 的 top(負值):讓角標突出卡片上方4.2 彈跳視窗置中 + 內容留白/* 遮罩層(不用定位,用固定定位覆蓋全螢幕) */ .overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.5); } /* 彈跳視窗本體 */ .modal { position: fixed; /* 相對視窗定位 */ top: 50%; left: 50%; transform: translate(-50%, -50%); /* 完美置中 */ width: 90%; max-width: 500px; padding: 30px; /* 內容與邊框留白 */ background: white; box-sizing: border-box; /* 確保 padding 不會讓寬度爆掉 */ } .modal-title { margin-bottom: 20px; /* 標題與內文間距 */ line-height: 1.3; /* 標題行高 */ } .modal-content { line-height: 1.6; /* 內文行高 */ } 技巧拆解:position: fixed + transform:讓視窗永遠在畫面中央,不受捲動影響 pjchender.githubpadding + box-sizing: border-box:確保內容留白不會破壞寬度計算margin-bottom:用於內部元素之間的間距line-height:提升文字可讀性4.3 固定導覽列 + 內容偏移/* 固定在頂部的導覽列 */ .navbar { position: fixed; top: 0; left: 0; width: 100%; height: 60px; padding: 0 20px; /* 左右內邊距 */ background: white; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } /* 主要內容區 */ .main-content { margin-top: 60px; /* 留出導覽列的高度,避免被遮住 */ padding: 20px; /* 內容區留白 */ } 為什麼用 margin-top 而非 padding-top?navbar 使用 position: fixed 會脫離文件流主內容區會跑到畫面頂部被遮住用 margin-top 推擠主內容區,讓它從導覽列下方開始顯示 web五、易錯提醒與除錯技巧5.1 定位基準找錯/* ❌ 錯誤:子元素會以 body 為基準 */ .parent { /* 沒有設定 position */ } .child { position: absolute; top: 20px; /* 會從 body 頂部開始算 */ } /* ✅ 正確:子元素以父元素為基準 */ .parent { position: relative; /* 建立參考點 */ } .child { position: absolute; top: 20px; /* 從父元素頂部開始算 */ } 5.2 padding 讓 absolute 子元素位移.parent { position: relative; padding: 30px; /* 注意這個 padding */ } .child { position: absolute; top: 0; left: 0; /* 實際位置會距離父元素 border 內緣 30px(受 padding 影響) */ } /* 如果想讓子元素貼齊 border 內緣,用負值抵消 */ .child { position: absolute; top: -30px; /* 往上抵消 padding */ left: -30px; /* 往左抵消 padding */ } 5.3 box-sizing 影響定位計算.box { box-sizing: border-box; width: 200px; padding: 20px; border: 10px solid black; /* 實際可定位區域 = 200px(包含 padding 和 border) */ } .box-child { position: absolute; width: 100%; /* 這裡的 100% = 200px,不是 content 寬度 */ } ## 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