線上課程觀課進度管理小工具開發日誌
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
線上課程觀課進度管理小工具開發日誌
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」。
實作 askOneRound(單輪問答)+ 重構 runCliQuestions 支援 rounds 參數,完成多輪輸入的設計。確認邊界測試、單元測試、整合測試的設計方式和考量。
在上一篇筆記(2025/12/24)中,完成了 runCliQuestions 紅燈 5 的實作,並發現回傳結構 [{age: 25}, {name: 'Alice'}] 語意不清。
決定拆分成兩層 API:
askOneRound:一輪問答 → 單一物件
runCliQuestions:多輪收集 → 物件陣列(預設 1 輪)
本篇記錄實作過程。
// util/runCliQuestions.test.js
import { describe, it, expect, vi } from "vitest";
import { runCliQuestions } from "./runCliQuestions.js";
describe("runCliQuestions", () => {
it("紅燈 6:呼叫 askWithValidator 的回傳值組成單一物件", async () => {
const questionConfigs = [
{
key: "age",
prompt: "請輸入年齡:",
validator: vi.fn(),
},
{
key: "name",
prompt: "請輸入姓名:",
validator: vi.fn(),
},
];
// Mock askWithValidator 的行為
const mockAskWithValidator = vi
.fn()
.mockResolvedValueOnce(25)
.mockResolvedValueOnce("Alice");
const result = await runCliQuestions(questionConfigs, mockAskWithValidator);
// 驗證:有沒有正確呼叫 askWithValidator
expect(mockAskWithValidator).toHaveBeenCalledTimes(2);
expect(mockAskWithValidator).toHaveBeenNthCalledWith(
1,
"請輸入年齡:",
questionConfigs[0].validator
);
expect(mockAskWithValidator).toHaveBeenNthCalledWith(
2,
"請輸入姓名:",
questionConfigs[1].validator
);
expect(result).toEqual({ age: 25, name: "Alice" });
});
});
// util/runCliQuestions.js
export async function runCliQuestions(questionConfigs, askWithValidator){
const answers = {};
for(const config of questionConfigs){
const answer = await askWithValidator(config.prompt, config.validator);
answers[config.key]= answer;
}
return answers;
}
// util/runCliQuestions.test.js
import { describe, it, expect, vi } from "vitest";
import { askOneRound, runCliQuestions } from "./runCliQuestions.js";
describe("askOneRound", () => {
it("紅燈 7:抽出 askOneRound (原來的 runCliQuestions 更名)", async () => {
const questionConfigs = [
{
key: "age",
prompt: "請輸入年齡:",
validator: vi.fn(),
},
{
key: "name",
prompt: "請輸入姓名:",
validator: vi.fn(),
},
];
// Mock askWithValidator 的行為
const mockAskWithValidator = vi
.fn()
.mockResolvedValueOnce(25)
.mockResolvedValueOnce("Alice");
const result = await askOneRound(questionConfigs, mockAskWithValidator);
// 驗證:有沒有正確呼叫 askWithValidator
expect(mockAskWithValidator).toHaveBeenCalledTimes(2);
expect(mockAskWithValidator).toHaveBeenNthCalledWith(
1,
"請輸入年齡:",
questionConfigs[0].validator
);
expect(mockAskWithValidator).toHaveBeenNthCalledWith(
2,
"請輸入姓名:",
questionConfigs[1].validator
);
expect(result).toEqual({ age: 25, name: "Alice" });
});
});
// util/runCliQuestions.js
export async function askOneRound(questionConfigs, askWithValidator){
const answers = {};
for(const config of questionConfigs){
const answer = await askWithValidator(config.prompt, config.validator);
answers[config.key]= answer;
}
return answers;
}
describe("runCliQuestions", () => {
it("紅燈 8:定義 runCliQuestions", async () => {
const questionConfigs = [
{
key: "age",
prompt: "請輸入年齡:",
validator: vi.fn(),
},
{
key: "name",
prompt: "請輸入姓名:",
validator: vi.fn(),
},
];
// Mock askWithValidator 的行為
const mockAskWithValidator = vi
.fn()
.mockResolvedValueOnce(25)
.mockResolvedValueOnce("Alice");
const results = await runCliQuestions(questionConfigs, mockAskWithValidator);
expect(results).toEqual('ok')
})
})
export async function runCliQuestions(questionConfigs, askWithValidator, rounds = 1){
return 'ok'
}
describe("runCliQuestions", () => {
it("紅燈 9:呼叫 askOneRound 一次,回傳含物件的陣列", async () => {
const questionConfigs = [
{
key: "age",
prompt: "請輸入年齡:",
validator: vi.fn(),
},
{
key: "name",
prompt: "請輸入姓名:",
validator: vi.fn(),
},
];
// Mock askWithValidator 的行為
const mockAskWithValidator = vi
.fn()
.mockResolvedValueOnce(25)
.mockResolvedValueOnce("Alice");
const results = await runCliQuestions(questionConfigs, mockAskWithValidator);
expect(results).toEqual([{ age: 25, name: "Alice" }])
})
})
export async function runCliQuestions(questionConfigs, askWithValidator, rounds = 1){
const oneRoundResult = await askOneRound(questionConfigs, askWithValidator);
return [oneRoundResult]
}
// 用工廠函式建立獨立測試資料
function createBaseQuestionConfigs() {
return [
{
key: 'age',
prompt: '請輸入年齡:',
validator: vi.fn(),
},
{
key: 'name',
prompt: '請輸入姓名:',
validator: vi.fn(),
},
];
}
describe("runCliQuestions", () => {
it("紅燈 10:呼叫 askOneRound 兩次,回傳含物件的陣列", async () => {
const questionConfigs = createBaseQuestionConfigs();
// Mock askWithValidator 的行為
const mockAskWithValidator = vi
.fn()
// 第一輪
.mockResolvedValueOnce(25)
.mockResolvedValueOnce("Alice")
// 第二輪
.mockResolvedValueOnce(30)
.mockResolvedValueOnce("Bob");
const results = await runCliQuestions(
questionConfigs,
mockAskWithValidator,
2
);
expect(mockAskWithValidator).toHaveBeenCalledTimes(4);
expect(results).toEqual([{ age: 25, name: "Alice" }, { age: 30, name: "Bob" },]);
});
});
export async function runCliQuestions(
questionConfigs,
askWithValidator,
rounds = 1
) {
const result = [];
for (let round = 0; round < rounds; round++) {
const oneRoundResult = await askOneRound(questionConfigs, askWithValidator);
result.push(oneRoundResult);
}
return result;
}
到這邊完成 Happy path 測試和模組化函式實作完成
可以試著用一句話描述這個函式的行為,再從這句話裡圈出「最小」「最大」「例外」幾個關鍵詞,把「允許的極端情況」挑出來,當成候選的邊界條件。 像 rounds 不能是負數、不能是字串這種「輸入是否合法」,比較適合在更外層(例如 CLI 解析或型別系統)處理,不一定要在 runCliQuestions 這層當邊界條件測。
以 runCliQuestions 為例: 「在給定一組問題設定 questionConfigs 與 askWithValidator 的前提下,執行 rounds 輪問答,每輪使用同一組題目,並依順序回傳每輪的答案物件陣列。」
從這句話可以挖出幾個邊界:
「每輪使用同一組題目」→ questionConfigs 空/多題/一題,都屬於這層的語意範圍。
「執行 rounds 輪」→ rounds 為 0 時到底算不算「執行」,要不要定義成「回傳空陣列」。
「依順序回傳」→ 多輪時順序錯亂就是邊界錯誤。
挑那些「很接近這個函式核心責任、出錯代價會很痛」的邊界,給它一個獨立的小紅燈。
runCliQuestions 可能會有幾個邊界條件:
questionConfigs :很接近「問答流程與結果結構」的核心責任。如果是空陣列時,一輪結果是不是 {},多輪是不是 [{}, {}]。 - rounds = 0 :屬於「這一層要不要允許、允許就定義行為」的設計點。視設計決定是否支援,不支援就由上層擋掉。定義可以是回傳空陣列[](或自行定義其他形式報錯)。
askOneRound 當 askWithValidator 拋錯時,錯誤會直接往外拋,不被吞掉(對齊之後的錯誤邊界設計):屬於重要的行為保證。
單元測試:針對單一函式或模組,隔離其他依賴(用 mock / stub 替身),驗證「這個單元本身的邏輯是否正確」。
整合測試:讓多個模組一起運作,驗證「這些模組之間的協作與資料傳遞是否如預期」,通常會接觸真實或接近真實的依賴(例如檔案系統、資料庫、外部 API)。
整體設計:
runCliSession (錯誤邊界 / readline 生命週期)
↓
runCliQuestions (問題流程編排,多輪)
↓
askOneRound (單輪多題 → 物件)
↓
askWithValidator (單題 + 重試)
↓
validators / composeValidators (純值驗證)
validators / composeValidators
測試重點:給定特定輸入,回傳 { value } 或丟 Error。
不需要任何外部依賴,純函式邏輯。
askOneRound / runCliQuestions
測試重點:用 vi.fn() mock askWithValidator,驗證:
呼叫次數、參數是否正確。
回傳的資料結構(單一物件 / 物件陣列)是否符合設計。
不實際啟動 readline 或呼叫真實 validators,完全隔離依賴。
優點:
執行速度快(無 I/O、無外部系統)。
容易針對各種輸入組合(空陣列、多輪、單題)寫出精確測試。
當邏輯改動時,測試失敗能快速定位到哪一個函式出問題。
askWithValidator + 真實 readline
測試重點:真的啟動一個 Node.js readline 實例(或用 helper 模擬 stdin),驗證:
使用者輸入無效值時,會重新提示。
輸入有效值時,會正確回傳 parsed 結果。
runCliSession 整體流程
測試重點:從 CLI entrypoint 啟動一個完整 session,模擬使用者輸入一系列答案,驗證:
多輪問答能依序執行。
錯誤邊界能正確捕捉並顯示友善訊息。
最終輸出符合預期格式(例如 JSON 或計算結果)。
屬於最接近「真實使用者操作」的測試,會串起所有模組,偏向 end-to-end。
多數專案會遵循「測試金字塔」:大量單元測試、較少整合測試、少量 E2E 測試。
對這個 CLI 專案,可以採取:
優先寫單元測試
對每個模組(validators、askOneRound、runCliQuestions)寫快速、隔離的單元測試,涵蓋 Happy path 與重要邊界。
用 mock 隔離依賴,測試重點放在「資料結構轉換」和「流程編排邏輯」是否正確。
針對關鍵路徑補整合測試
只針對「最容易出錯、牽涉多層協作」的部分寫整合測試,例如:
askWithValidator 真的接上 readline 與 validators,確認重試邏輯正常。
runCliSession 整個流程跑一遍,確認錯誤邊界與多輪輸入能正常收尾。
這些測試數量較少,但能補足單元測試無法覆蓋的「模組之間介面不匹配」或「真實環境行為」問題。
用 Vitest 的 workspace / projects 分開跑
可以在 vitest.config.ts 設定不同的 projects,讓單元測試與整合測試分開執行:
import { defineWorkspace } from "vitest/config";
export default defineWorkspace([
{
test: {
name: "unit",
include: ["**/*.unit.test.js"],
},
},
{
test: {
name: "integration",
include: ["**/*.integration.test.js"],
},
},
]);
這樣可以在 CI 或開發時:
頻繁跑快速的單元測試。
定期跑較慢的整合測試(例如每次 commit 或 PR)。
情境 | 優先選擇 |
|---|---|
純邏輯運算、資料轉換(如 validators) | 單元測試 |
流程編排、依賴其他函式但可 mock(如 runCliQuestions) | 單元測試 |
需要確認「真實 I/O」或「外部依賴」行為(如 readline、檔案讀寫) | 整合測試 |
需要驗證「整條使用者流程」或「錯誤邊界是否正確攔截」 | 整合測試 / E2E |
剛開始開發、設計還在調整 | 優先單元測試,等穩定後再補整合 |
目前已完成的 validators、askOneRound、runCliQuestions 都用單元測試覆蓋 Happy path,這是最划算、最快的作法。
接下來實作 runCliSession 或 askWithValidator 真實接 readline 時,會需要補少量整合測試,確認「模組之間的協作」與「真實環境行為」符合預期。
目標不是「所有測試都寫成整合測試」,而是:
用大量單元測試保證每個模組邏輯正確。
用少量整合測試保證模組之間介面能接起來、真實環境能跑。
讓測試既快又穩,維護成本也低。
實作 askOneRound(單輪問答)+ 重構 runCliQuestions 支援 rounds 參數,完成多輪輸入的設計。確認邊界測試、單元測試、整合測試的設計方式和考量。
在上一篇筆記(2025/12/24)中,完成了 runCliQuestions 紅燈 5 的實作,並發現回傳結構 [{age: 25}, {name: 'Alice'}] 語意不清。
決定拆分成兩層 API:
askOneRound:一輪問答 → 單一物件
runCliQuestions:多輪收集 → 物件陣列(預設 1 輪)
本篇記錄實作過程。
// util/runCliQuestions.test.js
import { describe, it, expect, vi } from "vitest";
import { runCliQuestions } from "./runCliQuestions.js";
describe("runCliQuestions", () => {
it("紅燈 6:呼叫 askWithValidator 的回傳值組成單一物件", async () => {
const questionConfigs = [
{
key: "age",
prompt: "請輸入年齡:",
validator: vi.fn(),
},
{
key: "name",
prompt: "請輸入姓名:",
validator: vi.fn(),
},
];
// Mock askWithValidator 的行為
const mockAskWithValidator = vi
.fn()
.mockResolvedValueOnce(25)
.mockResolvedValueOnce("Alice");
const result = await runCliQuestions(questionConfigs, mockAskWithValidator);
// 驗證:有沒有正確呼叫 askWithValidator
expect(mockAskWithValidator).toHaveBeenCalledTimes(2);
expect(mockAskWithValidator).toHaveBeenNthCalledWith(
1,
"請輸入年齡:",
questionConfigs[0].validator
);
expect(mockAskWithValidator).toHaveBeenNthCalledWith(
2,
"請輸入姓名:",
questionConfigs[1].validator
);
expect(result).toEqual({ age: 25, name: "Alice" });
});
});
// util/runCliQuestions.js
export async function runCliQuestions(questionConfigs, askWithValidator){
const answers = {};
for(const config of questionConfigs){
const answer = await askWithValidator(config.prompt, config.validator);
answers[config.key]= answer;
}
return answers;
}
// util/runCliQuestions.test.js
import { describe, it, expect, vi } from "vitest";
import { askOneRound, runCliQuestions } from "./runCliQuestions.js";
describe("askOneRound", () => {
it("紅燈 7:抽出 askOneRound (原來的 runCliQuestions 更名)", async () => {
const questionConfigs = [
{
key: "age",
prompt: "請輸入年齡:",
validator: vi.fn(),
},
{
key: "name",
prompt: "請輸入姓名:",
validator: vi.fn(),
},
];
// Mock askWithValidator 的行為
const mockAskWithValidator = vi
.fn()
.mockResolvedValueOnce(25)
.mockResolvedValueOnce("Alice");
const result = await askOneRound(questionConfigs, mockAskWithValidator);
// 驗證:有沒有正確呼叫 askWithValidator
expect(mockAskWithValidator).toHaveBeenCalledTimes(2);
expect(mockAskWithValidator).toHaveBeenNthCalledWith(
1,
"請輸入年齡:",
questionConfigs[0].validator
);
expect(mockAskWithValidator).toHaveBeenNthCalledWith(
2,
"請輸入姓名:",
questionConfigs[1].validator
);
expect(result).toEqual({ age: 25, name: "Alice" });
});
});
// util/runCliQuestions.js
export async function askOneRound(questionConfigs, askWithValidator){
const answers = {};
for(const config of questionConfigs){
const answer = await askWithValidator(config.prompt, config.validator);
answers[config.key]= answer;
}
return answers;
}
describe("runCliQuestions", () => {
it("紅燈 8:定義 runCliQuestions", async () => {
const questionConfigs = [
{
key: "age",
prompt: "請輸入年齡:",
validator: vi.fn(),
},
{
key: "name",
prompt: "請輸入姓名:",
validator: vi.fn(),
},
];
// Mock askWithValidator 的行為
const mockAskWithValidator = vi
.fn()
.mockResolvedValueOnce(25)
.mockResolvedValueOnce("Alice");
const results = await runCliQuestions(questionConfigs, mockAskWithValidator);
expect(results).toEqual('ok')
})
})
export async function runCliQuestions(questionConfigs, askWithValidator, rounds = 1){
return 'ok'
}
describe("runCliQuestions", () => {
it("紅燈 9:呼叫 askOneRound 一次,回傳含物件的陣列", async () => {
const questionConfigs = [
{
key: "age",
prompt: "請輸入年齡:",
validator: vi.fn(),
},
{
key: "name",
prompt: "請輸入姓名:",
validator: vi.fn(),
},
];
// Mock askWithValidator 的行為
const mockAskWithValidator = vi
.fn()
.mockResolvedValueOnce(25)
.mockResolvedValueOnce("Alice");
const results = await runCliQuestions(questionConfigs, mockAskWithValidator);
expect(results).toEqual([{ age: 25, name: "Alice" }])
})
})
export async function runCliQuestions(questionConfigs, askWithValidator, rounds = 1){
const oneRoundResult = await askOneRound(questionConfigs, askWithValidator);
return [oneRoundResult]
}
// 用工廠函式建立獨立測試資料
function createBaseQuestionConfigs() {
return [
{
key: 'age',
prompt: '請輸入年齡:',
validator: vi.fn(),
},
{
key: 'name',
prompt: '請輸入姓名:',
validator: vi.fn(),
},
];
}
describe("runCliQuestions", () => {
it("紅燈 10:呼叫 askOneRound 兩次,回傳含物件的陣列", async () => {
const questionConfigs = createBaseQuestionConfigs();
// Mock askWithValidator 的行為
const mockAskWithValidator = vi
.fn()
// 第一輪
.mockResolvedValueOnce(25)
.mockResolvedValueOnce("Alice")
// 第二輪
.mockResolvedValueOnce(30)
.mockResolvedValueOnce("Bob");
const results = await runCliQuestions(
questionConfigs,
mockAskWithValidator,
2
);
expect(mockAskWithValidator).toHaveBeenCalledTimes(4);
expect(results).toEqual([{ age: 25, name: "Alice" }, { age: 30, name: "Bob" },]);
});
});
export async function runCliQuestions(
questionConfigs,
askWithValidator,
rounds = 1
) {
const result = [];
for (let round = 0; round < rounds; round++) {
const oneRoundResult = await askOneRound(questionConfigs, askWithValidator);
result.push(oneRoundResult);
}
return result;
}
到這邊完成 Happy path 測試和模組化函式實作完成
可以試著用一句話描述這個函式的行為,再從這句話裡圈出「最小」「最大」「例外」幾個關鍵詞,把「允許的極端情況」挑出來,當成候選的邊界條件。 像 rounds 不能是負數、不能是字串這種「輸入是否合法」,比較適合在更外層(例如 CLI 解析或型別系統)處理,不一定要在 runCliQuestions 這層當邊界條件測。
以 runCliQuestions 為例: 「在給定一組問題設定 questionConfigs 與 askWithValidator 的前提下,執行 rounds 輪問答,每輪使用同一組題目,並依順序回傳每輪的答案物件陣列。」
從這句話可以挖出幾個邊界:
「每輪使用同一組題目」→ questionConfigs 空/多題/一題,都屬於這層的語意範圍。
「執行 rounds 輪」→ rounds 為 0 時到底算不算「執行」,要不要定義成「回傳空陣列」。
「依順序回傳」→ 多輪時順序錯亂就是邊界錯誤。
挑那些「很接近這個函式核心責任、出錯代價會很痛」的邊界,給它一個獨立的小紅燈。
runCliQuestions 可能會有幾個邊界條件:
questionConfigs :很接近「問答流程與結果結構」的核心責任。如果是空陣列時,一輪結果是不是 {},多輪是不是 [{}, {}]。 - rounds = 0 :屬於「這一層要不要允許、允許就定義行為」的設計點。視設計決定是否支援,不支援就由上層擋掉。定義可以是回傳空陣列[](或自行定義其他形式報錯)。
askOneRound 當 askWithValidator 拋錯時,錯誤會直接往外拋,不被吞掉(對齊之後的錯誤邊界設計):屬於重要的行為保證。
單元測試:針對單一函式或模組,隔離其他依賴(用 mock / stub 替身),驗證「這個單元本身的邏輯是否正確」。
整合測試:讓多個模組一起運作,驗證「這些模組之間的協作與資料傳遞是否如預期」,通常會接觸真實或接近真實的依賴(例如檔案系統、資料庫、外部 API)。
整體設計:
runCliSession (錯誤邊界 / readline 生命週期)
↓
runCliQuestions (問題流程編排,多輪)
↓
askOneRound (單輪多題 → 物件)
↓
askWithValidator (單題 + 重試)
↓
validators / composeValidators (純值驗證)
validators / composeValidators
測試重點:給定特定輸入,回傳 { value } 或丟 Error。
不需要任何外部依賴,純函式邏輯。
askOneRound / runCliQuestions
測試重點:用 vi.fn() mock askWithValidator,驗證:
呼叫次數、參數是否正確。
回傳的資料結構(單一物件 / 物件陣列)是否符合設計。
不實際啟動 readline 或呼叫真實 validators,完全隔離依賴。
優點:
執行速度快(無 I/O、無外部系統)。
容易針對各種輸入組合(空陣列、多輪、單題)寫出精確測試。
當邏輯改動時,測試失敗能快速定位到哪一個函式出問題。
askWithValidator + 真實 readline
測試重點:真的啟動一個 Node.js readline 實例(或用 helper 模擬 stdin),驗證:
使用者輸入無效值時,會重新提示。
輸入有效值時,會正確回傳 parsed 結果。
runCliSession 整體流程
測試重點:從 CLI entrypoint 啟動一個完整 session,模擬使用者輸入一系列答案,驗證:
多輪問答能依序執行。
錯誤邊界能正確捕捉並顯示友善訊息。
最終輸出符合預期格式(例如 JSON 或計算結果)。
屬於最接近「真實使用者操作」的測試,會串起所有模組,偏向 end-to-end。
多數專案會遵循「測試金字塔」:大量單元測試、較少整合測試、少量 E2E 測試。
對這個 CLI 專案,可以採取:
優先寫單元測試
對每個模組(validators、askOneRound、runCliQuestions)寫快速、隔離的單元測試,涵蓋 Happy path 與重要邊界。
用 mock 隔離依賴,測試重點放在「資料結構轉換」和「流程編排邏輯」是否正確。
針對關鍵路徑補整合測試
只針對「最容易出錯、牽涉多層協作」的部分寫整合測試,例如:
askWithValidator 真的接上 readline 與 validators,確認重試邏輯正常。
runCliSession 整個流程跑一遍,確認錯誤邊界與多輪輸入能正常收尾。
這些測試數量較少,但能補足單元測試無法覆蓋的「模組之間介面不匹配」或「真實環境行為」問題。
用 Vitest 的 workspace / projects 分開跑
可以在 vitest.config.ts 設定不同的 projects,讓單元測試與整合測試分開執行:
import { defineWorkspace } from "vitest/config";
export default defineWorkspace([
{
test: {
name: "unit",
include: ["**/*.unit.test.js"],
},
},
{
test: {
name: "integration",
include: ["**/*.integration.test.js"],
},
},
]);
這樣可以在 CI 或開發時:
頻繁跑快速的單元測試。
定期跑較慢的整合測試(例如每次 commit 或 PR)。
情境 | 優先選擇 |
|---|---|
純邏輯運算、資料轉換(如 validators) | 單元測試 |
流程編排、依賴其他函式但可 mock(如 runCliQuestions) | 單元測試 |
需要確認「真實 I/O」或「外部依賴」行為(如 readline、檔案讀寫) | 整合測試 |
需要驗證「整條使用者流程」或「錯誤邊界是否正確攔截」 | 整合測試 / E2E |
剛開始開發、設計還在調整 | 優先單元測試,等穩定後再補整合 |
目前已完成的 validators、askOneRound、runCliQuestions 都用單元測試覆蓋 Happy path,這是最划算、最快的作法。
接下來實作 runCliSession 或 askWithValidator 真實接 readline 時,會需要補少量整合測試,確認「模組之間的協作」與「真實環境行為」符合預期。
目標不是「所有測試都寫成整合測試」,而是:
用大量單元測試保證每個模組邏輯正確。
用少量整合測試保證模組之間介面能接起來、真實環境能跑。
讓測試既快又穩,維護成本也低。
Share Dialog
Share Dialog
No comments yet