| 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 (最優先)
目的: 從使用者角度測試完整功能
範例: 測試完整的戰鬥流程
import { test, expect } from 'vitest';
import { render, screen, userEvent } from '@testing-library/react';
import { App } from '@/App';
test('玩家可以啟動戰鬥並看到 AI 施放技能', async () => {
render(<App />);
await userEvent.click(screen.getByRole('button', { name: /前往野外/i }));
expect(screen.getByText(/遭遇敵人/i)).toBeInTheDocument();
await screen.findByText(/閱讀卷軸/i, {}, { timeout: 5000 });
expect(screen.getByText(/MP: 95\/100/i)).toBeInTheDocument();
});
2. Integration Test (次優先)
目的: 測試多個元件的協作
範例: 測試 BattleServer 與 BattleBridge 的整合
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 () => {
const config = {
aiModel: 'haiku',
prompt: '列出檔案',
workingDir: process.cwd()
};
const battleId = server.startBattle(config);
expect(server.battles.has(battleId)).toBe(true);
const output = await new Promise((resolve) => {
server.on('output', (id, data) => {
if (id === battleId) resolve(data);
});
});
expect(output).toBeTruthy();
});
});
3. Unit Test (最後)
目的: 測試單一函數或類別的邏輯
範例: 測試 OutputParser
import { describe, it, expect } from 'vitest';
import { OutputParser } from '@/server/OutputParser';
describe('OutputParser', () => {
const parser = new OutputParser();
it('應該解析 tool_use 事件為 skill_cast', () => {
const input = JSON.stringify({
type: 'tool_use',
name: 'Read',
input: { file_path: 'src/main.ts' }
});
const events = parser.parse(input);
expect(events).toHaveLength(1);
expect(events[0]).toMatchObject({
type: 'skill_cast',
skillName: '閱讀卷軸',
target: 'src/main.ts',
mpCost: 5
});
});
it('應該解析非 JSON 行為 dialogue 事件', () => {
const input = 'System initialized';
const events = parser.parse(input);
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) 設定
import { http, HttpResponse } from 'msw';
export const handlers = [
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'
});
})
];
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 範例
import { render, screen } from '@testing-library/react';
import { server } from '../setup';
import { http, HttpResponse } from 'msw';
it('應該顯示戰鬥狀態', async () => {
server.use(
http.get('/api/battles/test-id', () => {
return HttpResponse.json({
id: 'test-id',
hp: 75,
mp: 60,
status: 'active'
});
})
);
render(<BattleView battleId="test-id" />);
await screen.findByText(/HP: 75/i);
expect(screen.getByText(/MP: 60/i)).toBeInTheDocument();
});
重構規則
⚠️ 黃金法則:不可變更 expect
it('應該計算總 MP 消耗', () => {
const result = calculateTotalMP([10, 20, 30]);
expect(result).toBe(60);
expect(result).toBeGreaterThan(50);
});
it('應該計算總 MP 消耗', () => {
const result = calculateTotalMP([10, 20, 30]);
expect(result).toBe(60);
});
重構檢查清單
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);
}
詳細範例與常見陷阱
完整 TDD 範例 walkthrough、錯誤/正確對照、常見陷阱與修正在 references/examples-and-pitfalls.md。此處只列主要陷阱類別:
- 先寫完整實作再補測試(應先寫會失敗的測試)
- 一次 Green 太大步(拆成最小可通過實作)
- 測試內部實作細節(應測可觀察行為)
- 重構時改了 expect(expect 變動 = 行為變更,走另一個 RED → GREEN 循環)
詳見 references 的 ❌/✓ 對照範例。
開始實作前,確認:
開始實作後,持續檢查:
快速參考
TDD 循環
$ npm test
❌ Test failed
$ npm test
✓ Test passed
$ npm test
✓ Test still passed (expect 不變)
測試優先順序
Feature Test (最優先) → Integration Test → Unit Test
Test Double 優先順序
Fake (最優先) → Spy → Stub → Mock (最後)
重構黃金法則
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)
測試框架文檔:
記住:測試是設計工具,不是驗證工具。先寫測試,讓測試驅動你的設計!