線上課程觀課進度管理小工具開發日誌
Day 6:JavaScript 程式碼執行排序:遞迴函數、Call Stack、Task Queue
Call Stack;遞迴函數與 call stack 的關係;Task Queue(非同步的概念再釐清)
Day 9:「修煉圈圈 Practice Rings」Web app 開發日誌 - 2025 六角 AI Vibe Coding 體驗營期末大魔王作業
這份筆記旨在記錄 2025 六角 Vibe Coding 體驗營每日作業 Day 21 的期末專案成果,從一個簡單的個人痛點出發,透過與 AI(在我這個情境中是 Perplexity)協作,在一天內完成了一個包含前後端的全端網頁小工具:「修煉圈圈 Practice Rings」。
記錄雞蛋糕的每一步前端煉成過程,從小白到(也許是)前端工程師的學習與分享。

Subscribe to 雞蛋糕的前端修煉屋
線上課程觀課進度管理小工具開發日誌
Day 6:JavaScript 程式碼執行排序:遞迴函數、Call Stack、Task Queue
Call Stack;遞迴函數與 call stack 的關係;Task Queue(非同步的概念再釐清)
Day 9:「修煉圈圈 Practice Rings」Web app 開發日誌 - 2025 六角 AI Vibe Coding 體驗營期末大魔王作業
這份筆記旨在記錄 2025 六角 Vibe Coding 體驗營每日作業 Day 21 的期末專案成果,從一個簡單的個人痛點出發,透過與 AI(在我這個情境中是 Perplexity)協作,在一天內完成了一個包含前後端的全端網頁小工具:「修煉圈圈 Practice Rings」。
<100 subscribers
<100 subscribers
今日閱讀:《單元測試的藝術》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
功能 | Jest | Vitest | 差異說明 |
|---|---|---|---|
檔案路徑過濾 |
| 直接傳入檔案名稱 | Vitest 簡化語法 vitest |
測試名稱過濾 |
|
| 完全相同 vitest |
配置檔案 |
|
| 格式類似 vitest |
指定測試模式 |
|
| 語意相同 lutece.github |
排除測試 |
|
| 語意相同 lutece.github |
多專案配置 | - |
| Vitest 獨有 vitest |
測試標籤 | - |
| Vitest 4.1+ 獨有 main.vitest |
# 檔案路徑過濾
vitest basic # 執行包含 "basic" 的測試檔案
vitest auth login # 執行包含 "auth" 和 "login" 的檔案
# 測試名稱過濾
vitest -t "user login" # 根據測試名稱過濾
vitest --testNamePattern="auth.*" # 使用正則表達式
# 指定檔案與行號(Vitest 3+)
vitest basic/foo.test.ts:10 # 執行特定檔案的特定行測試
// 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, // 較長超時
},
},
],
},
})
{
"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
}
}
核心差異
單元測試:隔離測試個別元件,純邏輯運算,執行時間 < 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 分鐘,整合測試按需執行
幾秒鐘完成一輪循環
TDD 講究「紅燈 → 綠燈 → 重構」的快速迭代,理想循環時間為數秒鐘 。如果測試套件包含慢速整合測試,會打斷開發心流,失去即時回饋優勢。 itential
開發節奏對比
有測試分類:改代碼 → 2 秒後看到單元測試結果 → 繼續開發
無測試分類:改代碼 → 等待 5 分鐘全部測試跑完 → 心流中斷
隔離測試的穩定性
單元測試:不受外部環境影響,測試結果穩定可靠 vocus
整合測試:可能因網路波動、資料庫狀態、第三方 API 變更產生「片狀測試」(flaky test),需額外維護 stackoverflow
測試失敗的診斷效率
單元測試失敗:精準定位到特定函式問題
整合測試失敗:需檢查多個元件互動,診斷時間較長 codefresh
單元測試可能變成整合測試
隨著重構,原本的單元測試可能演變成整合測試: 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
與外部環境互動(資料庫、檔案系統、網路、時間)
需要多個真實元件協作
執行時間明顯較長
階段性品質閘門
設計多層測試閘門,越快的測試越早執行: 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 分鐘後失敗 → 已切換任務,修復成本高 provar
快速失敗策略
CI/CD 平台通常按運算時數計費,測試分類可避免浪費: 33rdsquare
// vitest.config.js - 設定失敗即停止
export default defineConfig({
test: {
bail: 1, // 第一個失敗就停止
},
})
效益範例
單元測試失敗:2 分鐘 × 成本 → 立即停止
不分類全跑:30 分鐘 × 成本 → 浪費 15 倍資源 codefresh
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 分鐘 33rdsquare |
精準匹配測試範圍
觸發情境 | 執行測試 | 時間 | 目的 |
|---|---|---|---|
本地提交前 | 單元測試 | 1-2 分鐘 | 快速驗證邏輯正確性 dzone |
Push 到分支 | 單元 + 整合 | 5-15 分鐘 | 確保元件協作正常 codefresh |
Pull 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"
}
}
針對不同測試類型設定重試策略
// 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%+,需持續優化片狀測試
環境隔離策略
# 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 過濾: vitest
src/
├── 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 分鐘)
定期檢視測試執行時間趨勢: 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),讓測試組織更靈活彈性,是前端工程師值得深入學習的工具。
參考資料
學習目標:理解傳統 CSS(不用 Flex/Grid)如何用留白 + 定位打造精準佈局
屬性 | 作用範圍 | 特性 |
|---|---|---|
| 元素與元素之間 | 不會有背景色,可以用負值,會發生合併(collapse) |
| 元素內容到邊框 | 會保留背景色,撐大可點擊範圍,影響總寬度 |
| 元素邊框 | 佔據空間,可設定顏色/樣式/粗細 |
| Flex/Grid 子元素間距 | 只作用於現代佈局,不會在容器邊緣產生空隙 |
屬性 | 作用對象 | 使用情境 |
|---|---|---|
| 文字行高 | 段落可讀性,單行置中,用純數字最佳(如 1.6) |
| 字元間距 | 標題視覺效果,中英文混排,Logo 設計 |
| 單詞間距 | 英文排版(對中文無效) |
| 段落首行縮排 | 傳統文章排版 |
| 空白字符處理 | 控制換行與空白顯示 |
屬性 | 計算邏輯 | 推薦值 |
|---|---|---|
| width 只算 content,padding/border 會額外增加 | 預設值(不推薦) |
| width = content + padding + border,往內推擠 | 全站設定(推薦) |
屬性 | 定位基準 | 是否脫離文件流 |
|---|---|---|
| 預設,按文件流排列 | 否 |
| 自己的原始位置 | 否(保留原空間) |
| 最近的已定位父元素 | 是(不佔空間) |
| 瀏覽器視窗 | 是(不佔空間) |
屬性 | 作用 | 搭配對象 |
|---|---|---|
| 指定元素距離定位基準的距離 |
|
| 相對自身尺寸位移 | 常與 |
屬性 | 作用 | 使用情境 |
|---|---|---|
| 假裝元素是表格儲存格 | 搭配 |
| 行內元素垂直對齊 | 只對 inline/inline-block/table-cell 有效 |
比喻:像在地上畫記號,告訴物件「從你原本的位置移動」。 steam.oxxostudio
.box {
position: relative;
top: 20px; /* 從原位置往下移 20px */
left: 30px; /* 從原位置往右移 30px */
}
特性: web
保留原空間:元素移走後,原位置仍保留空位(其他元素不會遞補)
不影響其他元素:位移只是視覺效果,不會推擠鄰居
常用於建立定位參考點:讓子元素的 absolute 以它為基準 vocus
比喻:像飛出文件流的氣球,可以飄到任何地方,不佔地面空間。 vocus
.parent {
position: relative; /* 建立參考點 */
}
.child {
position: absolute;
top: 0; /* 距離父元素上緣 0px */
right: 0; /* 距離父元素右緣 0px */
}
定位基準規則: eudora
有定位的父元素:找最近的 position: relative/absolute/fixed 的父元素
沒有定位的父元素:以 <body> 或初始包含區塊為基準 vocus
特性: eudora
脫離文件流:不佔原本空間,其他元素會遞補上來
不推擠其他元素:怎麼移動都不影響鄰居排列
寬高自動收縮:沒指定寬高時,會縮到內容大小 eudora
當元素使用 position: relative 時:
屬性組合 | 效果 | 適用情境 |
|---|---|---|
| 推擠其他元素,會影響佈局流 | 需要調整整體佈局 |
| 只移動自己,不影響其他元素 | 微調單一元素位置 |
/* 範例對比 */
.method-1 {
position: relative;
margin-top: 20px; /* 會把下方所有元素往下推 */
}
.method-2 {
position: relative;
top: 20px; /* 只有自己往下移,保留原空間 */
}
當元素使用 position: absolute 時: code2study.blogspot
margin 和 top/left 效果相同,因為元素已脫離文件流
推薦用 top/left:語意更清楚,表示「定位座標」
.parent {
position: relative;
padding: 50px; /* 父元素的 padding 會影響子元素的定位起點 */
border: 20px solid orange;
}
.child {
position: absolute;
top: 0; /* 起點是父元素 padding 內緣,不是 border 內緣 */
left: 0;
width: 100px;
height: 100px;
}
關鍵規則: zero-plus
absolute 子元素的定位起點 = 父元素的 padding 內緣
父元素的 padding 會「內推」子元素的 (0, 0) 起點
父元素的 margin 對子元素定位無影響
.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 會平均分配上下剩餘空間
水平方向同理
限制:子元素必須有明確的寬高,不適合響應式內容。 realnewbie
.box {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%); /* 百分比相對自身尺寸 */
}
關鍵差異: pjchender.github
top: 50%:相對父元素高度的 50%
transform: translateY(-50%):相對自身高度的 50%
transform 不會改變元素佔據的空間,只是視覺位移
.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(負值):讓角標突出卡片上方
/* 遮罩層(不用定位,用固定定位覆蓋全螢幕) */
.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.github
padding + box-sizing: border-box:確保內容留白不會破壞寬度計算
margin-bottom:用於內部元素之間的間距
line-height:提升文字可讀性
/* 固定在頂部的導覽列 */
.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
/* ❌ 錯誤:子元素會以 body 為基準 */
.parent {
/* 沒有設定 position */
}
.child {
position: absolute;
top: 20px; /* 會從 body 頂部開始算 */
}
/* ✅ 正確:子元素以父元素為基準 */
.parent {
position: relative; /* 建立參考點 */
}
.child {
position: absolute;
top: 20px; /* 從父元素頂部開始算 */
}
.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 */
}
.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 寬度 */
}
今日閱讀:《單元測試的藝術》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
功能 | Jest | Vitest | 差異說明 |
|---|---|---|---|
檔案路徑過濾 |
| 直接傳入檔案名稱 | Vitest 簡化語法 vitest |
測試名稱過濾 |
|
| 完全相同 vitest |
配置檔案 |
|
| 格式類似 vitest |
指定測試模式 |
|
| 語意相同 lutece.github |
排除測試 |
|
| 語意相同 lutece.github |
多專案配置 | - |
| Vitest 獨有 vitest |
測試標籤 | - |
| Vitest 4.1+ 獨有 main.vitest |
# 檔案路徑過濾
vitest basic # 執行包含 "basic" 的測試檔案
vitest auth login # 執行包含 "auth" 和 "login" 的檔案
# 測試名稱過濾
vitest -t "user login" # 根據測試名稱過濾
vitest --testNamePattern="auth.*" # 使用正則表達式
# 指定檔案與行號(Vitest 3+)
vitest basic/foo.test.ts:10 # 執行特定檔案的特定行測試
// 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, // 較長超時
},
},
],
},
})
{
"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
}
}
核心差異
單元測試:隔離測試個別元件,純邏輯運算,執行時間 < 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 分鐘,整合測試按需執行
幾秒鐘完成一輪循環
TDD 講究「紅燈 → 綠燈 → 重構」的快速迭代,理想循環時間為數秒鐘 。如果測試套件包含慢速整合測試,會打斷開發心流,失去即時回饋優勢。 itential
開發節奏對比
有測試分類:改代碼 → 2 秒後看到單元測試結果 → 繼續開發
無測試分類:改代碼 → 等待 5 分鐘全部測試跑完 → 心流中斷
隔離測試的穩定性
單元測試:不受外部環境影響,測試結果穩定可靠 vocus
整合測試:可能因網路波動、資料庫狀態、第三方 API 變更產生「片狀測試」(flaky test),需額外維護 stackoverflow
測試失敗的診斷效率
單元測試失敗:精準定位到特定函式問題
整合測試失敗:需檢查多個元件互動,診斷時間較長 codefresh
單元測試可能變成整合測試
隨著重構,原本的單元測試可能演變成整合測試: 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
與外部環境互動(資料庫、檔案系統、網路、時間)
需要多個真實元件協作
執行時間明顯較長
階段性品質閘門
設計多層測試閘門,越快的測試越早執行: 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 分鐘後失敗 → 已切換任務,修復成本高 provar
快速失敗策略
CI/CD 平台通常按運算時數計費,測試分類可避免浪費: 33rdsquare
// vitest.config.js - 設定失敗即停止
export default defineConfig({
test: {
bail: 1, // 第一個失敗就停止
},
})
效益範例
單元測試失敗:2 分鐘 × 成本 → 立即停止
不分類全跑:30 分鐘 × 成本 → 浪費 15 倍資源 codefresh
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 分鐘 33rdsquare |
精準匹配測試範圍
觸發情境 | 執行測試 | 時間 | 目的 |
|---|---|---|---|
本地提交前 | 單元測試 | 1-2 分鐘 | 快速驗證邏輯正確性 dzone |
Push 到分支 | 單元 + 整合 | 5-15 分鐘 | 確保元件協作正常 codefresh |
Pull 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"
}
}
針對不同測試類型設定重試策略
// 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%+,需持續優化片狀測試
環境隔離策略
# 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 過濾: vitest
src/
├── 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 分鐘)
定期檢視測試執行時間趨勢: 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),讓測試組織更靈活彈性,是前端工程師值得深入學習的工具。
參考資料
學習目標:理解傳統 CSS(不用 Flex/Grid)如何用留白 + 定位打造精準佈局
屬性 | 作用範圍 | 特性 |
|---|---|---|
| 元素與元素之間 | 不會有背景色,可以用負值,會發生合併(collapse) |
| 元素內容到邊框 | 會保留背景色,撐大可點擊範圍,影響總寬度 |
| 元素邊框 | 佔據空間,可設定顏色/樣式/粗細 |
| Flex/Grid 子元素間距 | 只作用於現代佈局,不會在容器邊緣產生空隙 |
屬性 | 作用對象 | 使用情境 |
|---|---|---|
| 文字行高 | 段落可讀性,單行置中,用純數字最佳(如 1.6) |
| 字元間距 | 標題視覺效果,中英文混排,Logo 設計 |
| 單詞間距 | 英文排版(對中文無效) |
| 段落首行縮排 | 傳統文章排版 |
| 空白字符處理 | 控制換行與空白顯示 |
屬性 | 計算邏輯 | 推薦值 |
|---|---|---|
| width 只算 content,padding/border 會額外增加 | 預設值(不推薦) |
| width = content + padding + border,往內推擠 | 全站設定(推薦) |
屬性 | 定位基準 | 是否脫離文件流 |
|---|---|---|
| 預設,按文件流排列 | 否 |
| 自己的原始位置 | 否(保留原空間) |
| 最近的已定位父元素 | 是(不佔空間) |
| 瀏覽器視窗 | 是(不佔空間) |
屬性 | 作用 | 搭配對象 |
|---|---|---|
| 指定元素距離定位基準的距離 |
|
| 相對自身尺寸位移 | 常與 |
屬性 | 作用 | 使用情境 |
|---|---|---|
| 假裝元素是表格儲存格 | 搭配 |
| 行內元素垂直對齊 | 只對 inline/inline-block/table-cell 有效 |
比喻:像在地上畫記號,告訴物件「從你原本的位置移動」。 steam.oxxostudio
.box {
position: relative;
top: 20px; /* 從原位置往下移 20px */
left: 30px; /* 從原位置往右移 30px */
}
特性: web
保留原空間:元素移走後,原位置仍保留空位(其他元素不會遞補)
不影響其他元素:位移只是視覺效果,不會推擠鄰居
常用於建立定位參考點:讓子元素的 absolute 以它為基準 vocus
比喻:像飛出文件流的氣球,可以飄到任何地方,不佔地面空間。 vocus
.parent {
position: relative; /* 建立參考點 */
}
.child {
position: absolute;
top: 0; /* 距離父元素上緣 0px */
right: 0; /* 距離父元素右緣 0px */
}
定位基準規則: eudora
有定位的父元素:找最近的 position: relative/absolute/fixed 的父元素
沒有定位的父元素:以 <body> 或初始包含區塊為基準 vocus
特性: eudora
脫離文件流:不佔原本空間,其他元素會遞補上來
不推擠其他元素:怎麼移動都不影響鄰居排列
寬高自動收縮:沒指定寬高時,會縮到內容大小 eudora
當元素使用 position: relative 時:
屬性組合 | 效果 | 適用情境 |
|---|---|---|
| 推擠其他元素,會影響佈局流 | 需要調整整體佈局 |
| 只移動自己,不影響其他元素 | 微調單一元素位置 |
/* 範例對比 */
.method-1 {
position: relative;
margin-top: 20px; /* 會把下方所有元素往下推 */
}
.method-2 {
position: relative;
top: 20px; /* 只有自己往下移,保留原空間 */
}
當元素使用 position: absolute 時: code2study.blogspot
margin 和 top/left 效果相同,因為元素已脫離文件流
推薦用 top/left:語意更清楚,表示「定位座標」
.parent {
position: relative;
padding: 50px; /* 父元素的 padding 會影響子元素的定位起點 */
border: 20px solid orange;
}
.child {
position: absolute;
top: 0; /* 起點是父元素 padding 內緣,不是 border 內緣 */
left: 0;
width: 100px;
height: 100px;
}
關鍵規則: zero-plus
absolute 子元素的定位起點 = 父元素的 padding 內緣
父元素的 padding 會「內推」子元素的 (0, 0) 起點
父元素的 margin 對子元素定位無影響
.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 會平均分配上下剩餘空間
水平方向同理
限制:子元素必須有明確的寬高,不適合響應式內容。 realnewbie
.box {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%); /* 百分比相對自身尺寸 */
}
關鍵差異: pjchender.github
top: 50%:相對父元素高度的 50%
transform: translateY(-50%):相對自身高度的 50%
transform 不會改變元素佔據的空間,只是視覺位移
.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(負值):讓角標突出卡片上方
/* 遮罩層(不用定位,用固定定位覆蓋全螢幕) */
.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.github
padding + box-sizing: border-box:確保內容留白不會破壞寬度計算
margin-bottom:用於內部元素之間的間距
line-height:提升文字可讀性
/* 固定在頂部的導覽列 */
.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
/* ❌ 錯誤:子元素會以 body 為基準 */
.parent {
/* 沒有設定 position */
}
.child {
position: absolute;
top: 20px; /* 會從 body 頂部開始算 */
}
/* ✅ 正確:子元素以父元素為基準 */
.parent {
position: relative; /* 建立參考點 */
}
.child {
position: absolute;
top: 20px; /* 從父元素頂部開始算 */
}
.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 */
}
.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 寬度 */
}
Share Dialog
Share Dialog
No activity yet