Skip to main content

tdd-guidelines

cc-office TDD workflow — Red-Green-Refactor cycle, expect-immutability rule during refactor, test pyramid, naming (AAA), and refactor discipline. Use when writing new code, refactoring, designing test structure, fixing failing tests in a principled way, or onboarding to this project's TDD conventions. Pairs with generic `tdd` skill and `test-doubles` theory.

跳到安装

来源信息

仓库
recca0120/code-quest
最近来源活动
2026年4月19日 22:46
检测到的 SKILL.md 语言
中文
星标
11
分支
2

安装方式

默认使用会先检查来源的 Prompt;你也可以切换为直接命令,或下载本地副本。

检查来源文件

决定是否安装前,请先阅读 SKILL.md,以及 SkillsMP 当前展示的配套文件。

文件资源管理器
2 个文件

正在显示 SKILL.md

SKILL.md
来源说明 · 只读预览
name
tdd-guidelines
description
cc-office TDD workflow — Red-Green-Refactor cycle, expect-immutability rule during refactor, test pyramid, naming (AAA), and refactor discipline. Use when writing new code, refactoring, designing test structure, fixing failing tests in a principled way, or onboarding to this project's TDD conventions. Pairs with generic `tdd` skill and `test-doubles` theory.
# TDD Guidelines - 測試驅動開發準則 ## 核心原則 Code Quest 採用 **嚴格的 TDD (Test-Driven Development)** 開發流程: > **先寫測試,逼出程式用法,再產生程式。絕不憑空想象。** --- ## TDD 開發循環 ``` ┌─────────────────────────────────────────┐ │ RED → GREEN → REFACTOR │ ├─────────────────────────────────────────┤ │ │ │ 1. 🔴 RED: 寫一個會失敗的測試 │ │ - 描述期望的行為 │ │ - 測試應該失敗 (因為功能還不存在) │ │ │ │ 2. 🟢 GREEN: 寫最少的程式碼讓測試通過 │ │ - 只寫必要的程式碼 │ │ - 不要過度設計 │ │ - 讓測試變綠就停止 │ │ │ │ 3. 🔵 REFACTOR: 重構改善程式碼品質 │ │ - ⚠️ 不可變更 expect │ │ - expect 是我們最重要的結果 │ │ - 只重構實作,不改變行為 │ │ │ └─────────────────────────────────────────┘ ``` --- ## 測試金字塔 (優先順序) ``` ┌─────────────┐ │ Feature │ 最優先 │ Test │ (End-to-End) ├─────────────┤ │ Integration │ 次優先 │ Test │ (Component) ├─────────────┤ │ Unit │ 最後 │ Test │ (Function) └─────────────┘ ``` ### 1. Feature Test (最優先) **目的**: 從使用者角度測試完整功能 **範例**: 測試完整的戰鬥流程 ```typescript // tests/features/battle.feature.test.ts import { test, expect } from 'vitest'; import { render, screen, userEvent } from '@testing-library/react'; import { App } from '@/App'; test('玩家可以啟動戰鬥並看到 AI 施放技能', async () => { // Given: 玩家在城鎮 render(<App />); // When: 玩家點擊「前往野外」 await userEvent.click(screen.getByRole('button', { name: /前往野外/i })); // Then: 觸發戰鬥 expect(screen.getByText(/遭遇敵人/i)).toBeInTheDocument(); // When: AI 施放技能 await screen.findByText(/閱讀卷軸/i, {}, { timeout: 5000 }); // Then: 顯示 MP 消耗 expect(screen.getByText(/MP: 95\/100/i)).toBeInTheDocument(); }); ``` ### 2. Integration Test (次優先) **目的**: 測試多個元件的協作 **範例**: 測試 BattleServer 與 BattleBridge 的整合 ```typescript // tests/integration/battle-server.test.ts import { describe, it, expect, beforeEach } from 'vitest'; import { BattleServer } from '@/server/BattleServer'; import { BattleBridge } from '@/server/BattleBridge'; describe('BattleServer Integration', () => { let server: BattleServer; beforeEach(() => { server = new BattleServer(); }); it('應該啟動戰鬥並接收 PTY 輸出', async () => { // Given: 戰鬥配置 const config = { aiModel: 'haiku', prompt: '列出檔案', workingDir: process.cwd() }; // When: 啟動戰鬥 const battleId = server.startBattle(config); // Then: 應該建立戰鬥 Session expect(server.battles.has(battleId)).toBe(true); // When: 等待 PTY 輸出 const output = await new Promise((resolve) => { server.on('output', (id, data) => { if (id === battleId) resolve(data); }); }); // Then: 應該收到輸出 expect(output).toBeTruthy(); }); }); ``` ### 3. Unit Test (最後) **目的**: 測試單一函數或類別的邏輯 **範例**: 測試 OutputParser ```typescript // tests/unit/output-parser.test.ts import { describe, it, expect } from 'vitest'; import { OutputParser } from '@/server/OutputParser'; describe('OutputParser', () => { const parser = new OutputParser(); it('應該解析 tool_use 事件為 skill_cast', () => { // Given: Claude 輸出 (NDJSON) const input = JSON.stringify({ type: 'tool_use', name: 'Read', input: { file_path: 'src/main.ts' } }); // When: 解析 const events = parser.parse(input); // Then: 應該產生 skill_cast 事件 expect(events).toHaveLength(1); expect(events[0]).toMatchObject({ type: 'skill_cast', skillName: '閱讀卷軸', target: 'src/main.ts', mpCost: 5 }); }); it('應該解析非 JSON 行為 dialogue 事件', () => { // Given: 純文字輸出 const input = 'System initialized'; // When: 解析 const events = parser.parse(input); // Then: 應該產生 dialogue 事件 expect(events).toHaveLength(1); expect(events[0]).toMatchObject({ type: 'dialogue', speaker: 'system', message: 'System initialized' }); }); }); ``` --- ## Test Double 選擇 本 skill 聚焦 TDD 流程。Test double 選擇依情境見專屬 skill: - **經典五型定義**(Dummy / Stub / Fake / Spy / Mock)→ `test-doubles` skill - **Client 測試**(React / socket / Zustand)→ `frontend-testing` skill - **Server 測試**(Node / socket.io / DB / CLI)→ `vitest-testing` skill 核心原則:**優先真實實作;需要隔離再選 double,層級越低越好**。重構時 expect 不變的黃金法則跟 test double 型別無關。 --- ## 前端測試策略 > **React component / hook / store 的細節 patterns** 參考 `frontend-testing` 與 `testing-best-practices` skill。 > 本節只涵蓋 TDD 流程裡的 MSW 設定範例。 > > **ky-based API client 測試** 用 `msw-fetch-mock` 套件(protocol-level intercept),詳見 `msw-fetch-mock` skill。 ### MSW (Mock Service Worker) 設定 ```typescript // tests/mocks/handlers.ts import { http, HttpResponse } from 'msw'; export const handlers = [ // WebSocket 連接 http.get('/ws', () => { return HttpResponse.json({ status: 'connected' }); }), // 啟動戰鬥 http.post('/api/battles', async ({ request }) => { const body = await request.json(); return HttpResponse.json({ battleId: 'test-battle-id', status: 'started' }); }), // 取得戰鬥狀態 http.get('/api/battles/:battleId', ({ params }) => { return HttpResponse.json({ id: params.battleId, hp: 100, mp: 80, status: 'active' }); }) ]; // tests/setup.ts import { setupServer } from 'msw/node'; import { handlers } from './mocks/handlers'; export const server = setupServer(...handlers); beforeAll(() => server.listen()); afterEach(() => server.resetHandlers()); afterAll(() => server.close()); ``` ### MSW Fetch Mock 範例 ```typescript // tests/components/BattleView.test.tsx import { render, screen } from '@testing-library/react'; import { server } from '../setup'; import { http, HttpResponse } from 'msw'; it('應該顯示戰鬥狀態', async () => { // Given: Mock API 回應 server.use( http.get('/api/battles/test-id', () => { return HttpResponse.json({ id: 'test-id', hp: 75, mp: 60, status: 'active' }); }) ); // When: 渲染元件 render(<BattleView battleId="test-id" />); // Then: 應該顯示正確的狀態 await screen.findByText(/HP: 75/i); expect(screen.getByText(/MP: 60/i)).toBeInTheDocument(); }); ``` --- ## 重構規則 ### ⚠️ 黃金法則:不可變更 expect ```typescript // ❌ 錯誤:重構時改變了 expect it('應該計算總 MP 消耗', () => { const result = calculateTotalMP([10, 20, 30]); // 重構前 expect(result).toBe(60); // 重構後 (錯誤!) expect(result).toBeGreaterThan(50); // ❌ 改變了期望 }); // ✅ 正確:只重構實作,不改變 expect it('應該計算總 MP 消耗', () => { const result = calculateTotalMP([10, 20, 30]); // 重構前後 expect 不變 expect(result).toBe(60); // ✅ 期望不變 // 可以重構 calculateTotalMP 的實作 // 但結果必須一樣 }); ``` ### 重構檢查清單 ```typescript // 重構前 function calculateMP(skills: Skill[]): number { let total = 0; for (let i = 0; i < skills.length; i++) { total += skills[i].mpCost; } return total; } // 重構後 function calculateMP(skills: Skill[]): number { return skills.reduce((sum, skill) => sum + skill.mpCost, 0); } // ✅ 檢查清單 // 1. expect 不變? → ✓ // 2. 所有測試還通過? → ✓ // 3. 行為完全一致? → ✓ // 4. 程式碼更簡潔? → ✓ ``` --- ## 詳細範例與常見陷阱 完整 TDD 範例 walkthrough、錯誤/正確對照、常見陷阱與修正在 `references/examples-and-pitfalls.md`。此處只列主要陷阱類別: - 先寫完整實作再補測試(應先寫會失敗的測試) - 一次 Green 太大步(拆成最小可通過實作) - 測試內部實作細節(應測可觀察行為) - 重構時改了 expect(expect 變動 = 行為變更,走另一個 RED → GREEN 循環) 詳見 references 的 ❌/✓ 對照範例。 開始實作前,確認: - [ ] **測試先行** - 是否先寫了失敗的測試? - [ ] **最小實作** - 是否只寫了讓測試通過的最少程式碼? - [ ] **測試金字塔** - 是否優先寫 Feature Test? - [ ] **Test Double** - 是否優先使用 Fake? - [ ] **expect 不變** - 重構時是否保持 expect 不變? - [ ] **避免過度設計** - 是否只實作當前測試需要的功能? 開始實作後,持續檢查: - [ ] **所有測試通過** - 綠燈? - [ ] **程式碼簡潔** - 可以重構嗎? - [ ] **行為一致** - 重構後行為相同嗎? - [ ] **expect 不變** - 重構時 expect 沒變? --- ## 快速參考 ### TDD 循環 ```bash # 1. RED $ npm test ❌ Test failed # 2. GREEN $ npm test ✓ Test passed # 3. REFACTOR $ npm test ✓ Test still passed (expect 不變) ``` ### 測試優先順序 ``` Feature Test (最優先) → Integration Test → Unit Test ``` ### Test Double 優先順序 ``` Fake (最優先) → Spy → Stub → Mock (最後) ``` ### 重構黃金法則 ```typescript // ⚠️ 重構時 expect 絕對不可變更 expect(result).toBe(expectedValue); // 永遠不變 ``` --- ## 相關 skill **TDD 與 test double 理論**: - `tdd` — 通用 TDD(Red-Green-Refactor、命名、金字塔) - `test-doubles` — 五型(Mock / Stub / Fake / Spy / Dummy)理論與選擇順序 **Test harness(具體工具)**: - `fake-summoner-server` — server 端 socket / pipeline / ProcessRunner harness - `fake-summoner-client` — client 端 `renderWithChannel` / `renderWithWorkspace` - `fixture-driven-tdd` — parser / protocol 用真實 CLI JSON fixture 驅動 **各層測試慣例**: - `vitest-testing` — server vitest 專案慣例(5-tier test double) - `frontend-testing` — client RTL 慣例(7-tier test double、query 優先序) - `testing-best-practices` — 共用 Vitest + RTL + MSW 慣例 - `cc-office-review` — cc-office 專案特定測試檢查 **專案脈絡**: - `project-overview` — 當前架構(roadmap 參考 `rpg-roadmap`) **測試框架文檔**: - Vitest: https://vitest.dev/ - Testing Library: https://testing-library.com/ - MSW: https://mswjs.io/ --- 記住:**測試是設計工具,不是驗證工具。先寫測試,讓測試驅動你的設計!**
在 GitHub 查看