Time Compass Capture-First TDD 測試套件深入分析¶
簡介:Capture-First TDD 的理念¶
Time Compass 採用 Capture-First TDD(先擷取後驅動)的測試哲學,這是一套突破傳統 Unit Test 與 Mock-Based 測試侷限的創新方法論。
核心問題背景¶
在開發高度依賴外部 API(Google Calendar、Moodle)的專案時,手動撰寫 Mock 會產生兩大痛點:
- Mock 幻覺:開發者自己捏造的 JSON 結構往往與真實 API 回應有落差,導致測試通過但實際整合時掛掉
- 開發迴圈極慢:每次程式碼改動都需要發出真實網路請求,導致測試執行時間從秒級跳升到分鐘級
核心決策¶
強制推行「攔截與快照真相」策略,區分兩層離線資料:
| 層級 | 位置 | 用途 | 特性 |
|---|---|---|---|
| L0 快照 (Snapshots) | tests/snapshots/ | Unit/Regression 測試 | 原始、未清洗的 Raw JSON,完全從真實 API 擷取 |
| L1 展示框架 (Fixtures) | assets/fixtures/ | Dev Mode 示範、前端開發 | 經清洗、脫敏的資料,供無憑證環境使用 |
優點¶
- ⚡ 光速回饋迴圈:單元測試套件在數百毫秒內執行完成
- 🔐 零憑證開發體驗:透過
MCP_DEV_MODE=1環境變數,評審/設計師無需任何 Token 即可完整驗證流程 - ✅ 消除 Mock 幻覺:確保測試資料與真實 API 結構一致
三層測試架構¶
Time Compass 實施嚴格的三層分離測試策略,每層有明確的職責邊界與資料來源。
架構圖¶
┌─────────────────────────────────────────────────────────────┐
│ UI/MCP Tools (Frontend) │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ L3: System Tests (端對端整合,MCP 工具鏈) │
│ • Location: tests/system/mcp_suite/ │
│ • Task: 驗證 MCP 工具註冊,模擬真實用戶對話 │
│ • Data: In-process FastMCP + Snapshot 資料 │
│ • Speed: ~1-5 秒 per test │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ L2: Integration Tests (領域邏輯與轉換) │
│ • Location: tests/unit/integrations/ │
│ • Task: 驗證 API 響應 → 內部模型 → TOON 序列化 │
│ • Data: tests/snapshots/ 中的 Raw JSON │
│ • Speed: ~100ms per test │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ L1: Unit Tests (單一函數/類別的行為) │
│ • Location: tests/unit/{services,domain,utils,mcp}/ │
│ • Task: 驗證 Monad、時間邏輯、資料驗證 │
│ • Data: 直接構建的簡單物件或 Fixture │
│ • Speed: ~1-10ms per test │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ L0: Live Tests (真實 API 通訊,擷取快照) │
│ • Location: tests/live/ │
│ • Task: 與真實 Google/Moodle API 通訊,產生新快照 │
│ • Data Requirement: Google OAuth Token + Moodle 帳密 │
│ • Speed: ~10-60 秒 per test (網路延遲) │
│ • Usage: 手動執行(`uv run pytest tests/live/`) │
│ • Output: tests/snapshots/ 中的 Raw JSON │
└─────────────────────────────────────────────────────────────┘
L1 層:單元測試 (Unit Tests)¶
🏠 位置 : tests/unit/
目錄結構:
tests/unit/
├── domain/
├── integrations/
├── planner/
├── services/
├── mcp/
└── utils/
L2 層:整合測試 (Integration Tests)¶
🔗 位置 : tests/unit/integrations/ 的專責測試
L3 層:系統測試 (System Tests)¶
🌐 位置 : tests/system/mcp_suite/
L0 層:Live 測試(真實 API)¶
🚀 位置 : tests/live/
Fixture 與 Snapshot 管理策略¶
數據層級的三分法¶
L0: Raw API Response (最源頭)
↓ 轉換
L1: Processed Snapshot (中間格式)
↓ 脫敏
L2: Demo Fixtures (展示用)
Snapshot 的生命週期¶
1️⃣ 擷取階段(Live Capture)¶
export MOODLE_ACCOUNT="your_account"
export MOODLE_PASSWORD="your_password"
uv run python tests/snapshots/capture_snapshots.py
2️⃣ 配置與管理¶
# tests/snapshots/lib/snapshot_config.py
SNAPSHOT_START = datetime(2025, 11, 1, 0, 0, 0, tzinfo=timezone.utc)
SNAPSHOT_END = datetime(2025, 11, 30, 23, 59, 59, tzinfo=timezone.utc)
3️⃣ 快照的讀取¶
# tests/unit/integrations/test_google_calendar.py
import json
from pathlib import Path
def test_parse_calendar_list():
snapshot_path = Path("tests/snapshots/google/calendar_list.json")
raw_data = json.loads(snapshot_path.read_text())
google_calendars = [GoogleCalendarListRead.from_dict(item) for item in raw_data["items"]]
assert len(google_calendars) > 0
Mock Context 與 Dev Mode 的關係¶
Dev Mode 架構¶
啟動方式:
export MCP_DEV_MODE=1
核心實現¶
# src/time_compass/mcp/dev_mode.py
def is_dev_mode() -> bool:
return os.environ.get("MCP_DEV_MODE") == "1"
async def get_mock_resource_context(view_date=None) -> ResourceContext:
"""模擬完整的 ResourceContext"""
cal_list_data = load_fixture("google/calendar_list.json")
events_data = load_fixture("google/calendar/events_list.json")
# ...
return ResourceContext(...)
MCP 工具中的 Dev Mode 分支¶
@mcp.tool()
async def list_tasklists() -> list[dict]:
"""List all Google Tasks"""
if is_dev_mode():
fixture = load_fixture("google/tasks/tasklists_list.json")
return {"items": fixture.get("items", []) if fixture else []}
# 正常模式
auth_provider = google_auth_provider()
result = await async_get_all_tasks(...)
# ...
常見測試片段與最佳實踐¶
1. 從 Snapshot 讀取並解析¶
# ✅ 推薦做法
def test_parse_google_events_from_snapshot():
snapshot = Path("tests/snapshots/google/events_list.json")
raw_data = json.loads(snapshot.read_text(encoding="utf-8"))
events_result = AllCalendarEventsResult.model_validate(raw_data)
assert len(events_result.items) > 0
assert all(hasattr(e, "id") for e in events_result.items)
2. Mock 非同步 API 調用¶
# ✅ 推薦做法
@pytest.mark.asyncio
async def test_calendar_fetch_with_result_monad():
from unittest.mock import AsyncMock, patch
from time_compass.utils.result import Ok, Err
mock_client = AsyncMock()
mock_auth = AsyncMock(return_value="Bearer token")
with patch("...batch_execute_async") as mock_batch:
expected_response = [Ok(AllCalendarEventsResult(...))]
mock_batch.return_value = expected_response
result = await async_get_all_events(
client=mock_client,
request=GoogleAllCalendarRequest(...),
google_auth_provider=mock_auth
)
assert is_ok(result)
3. 驗證異常與 Error Handling¶
# ✅ 推薦做法
@pytest.mark.asyncio
async def test_google_api_error_handling():
from time_compass.integrations.common.exceptions import GoogleAuthError
# 模擬 API 錯誤
with patch("...batch_execute_async") as mock_batch:
mock_batch.side_effect = GoogleAuthError("401 Unauthorized")
result = await async_get_all_events(...)
assert is_err(result)
assert isinstance(result.error, GoogleAuthError)
4. 禁止事項 ❌¶
# ❌ 禁止:手動捏造大型 Mock Dictionary
def test_calendar_list_bad():
mock_data = {
"items": [
{
"id": "primary",
# ... 100 行手捏 JSON
}
]
}
# ❌ 禁止:在測試中使用 print()
def test_planner_output():
result = planner.generate()
print(result) # ❌ 污染 pytest 輸出
CI/CD 整合與覆蓋率目標¶
本地執行測試¶
# 快速迴路(L1 層,< 3 秒)
uv run pytest tests/unit/ -v --tb=short
# 含覆蓋率報告
uv run pytest tests/unit/ --cov=src/time_compass --cov-report=html
# 僅運行特定測試
uv run pytest tests/unit/domain/test_interaction_context.py::test_interaction_context_validation -v
# 錯誤時停止
uv run pytest tests/unit/ -x
# 顯示最慢的 10 個測試
uv run pytest tests/unit/ --durations=10
覆蓋率目標¶
| 層級 | 目標 | 說明 |
|---|---|---|
| Domain 層 | ≥ 90% | 業務邏輯必須充分覆蓋 |
| Integration 層 | ≥ 85% | API 轉換邏輯需覆蓋多種情境 |
| Utilities 層 | ≥ 95% | 工具函式應接近 100% |
| MCP Tools 層 | ≥ 80% | 涵蓋主要工具與 Dev Mode 分支 |
| 整體 | ≥ 85% | 全專案目標 |
新增測試的步驟檢查表¶
📋 Pre-Implementation:問題復現與快照擷取¶
□ 1. 識別問題
□ 2. 撰寫 Live Test
□ 3. 保存 Snapshot
🔴 Red Phase:撰寫失敗測試¶
□ 1. 在 tests/unit/ 建立測試類別
□ 2. 從 Snapshot 讀取測試資料
□ 3. 編寫驗證邏輯,執行測試確認失敗
🟢 Green Phase:修正程式碼¶
□ 1. 定位 Bug 所在
□ 2. 修改程式碼
□ 3. 執行測試確認通過
🟡 Refactor Phase:提升程式碼品質¶
□ 1. 檢視修正的程式碼
□ 2. 擴充測試(邊際案例、負面測試)
□ 3. 清理臨時檔案
總結與推薦實踐¶
三層測試的協調運作¶
日常開發迴路:
1. 修改程式碼 → 2. uv run pytest tests/unit/ -x → 3. 檢查證據 → 4. 重複
新增功能或修復 Bug:
1. 執行 Live Test → 2. 提交快照 → 3. 撰寫 Unit Test → 4. 修正程式碼 → 5. 驗證
文檔版本:Phase 4 (Capture-First TDD 完整階段)
適用範圍:tests/ & src/time_compass/