332 lines
11 KiB
Markdown
332 lines
11 KiB
Markdown
# 延遲計數系統測試規格
|
||
|
||
**目的**: 將您的延遲計數需求轉換為可執行的測試案例
|
||
**測試範圍**: 跳過功能、排序邏輯、答錯處理、優先級管理
|
||
**測試方式**: Jest + React Testing Library
|
||
**最後更新**: 2025-10-03
|
||
|
||
---
|
||
|
||
## 🧪 **核心需求測試**
|
||
|
||
### **測試1: 跳過功能基礎邏輯**
|
||
```typescript
|
||
describe('延遲計數系統 - 跳過功能', () => {
|
||
test('當使用者在答題時點擊跳過,該題目會跳過', () => {
|
||
// 準備測試數據
|
||
const initialCards = [
|
||
{
|
||
id: '1',
|
||
word: 'evidence',
|
||
skipCount: 0,
|
||
wrongCount: 0,
|
||
isCompleted: false
|
||
}
|
||
]
|
||
const currentIndex = 0
|
||
|
||
// 執行跳過操作
|
||
const result = handleSkip(initialCards, currentIndex)
|
||
|
||
// 驗證結果
|
||
expect(result.updatedCards[0].skipCount).toBe(1)
|
||
expect(result.updatedCards[0].wrongCount).toBe(0) // 不影響答錯次數
|
||
expect(result.nextIndex).toBe(1) // 移動到下一題
|
||
expect(result.updatedCards[0].isCompleted).toBe(false) // 未完成狀態
|
||
})
|
||
|
||
test('同一題目可以多次跳過,次數累加', () => {
|
||
const cards = [
|
||
{ id: '1', word: 'test', skipCount: 2, wrongCount: 1, isCompleted: false }
|
||
]
|
||
|
||
const result = handleSkip(cards, 0)
|
||
|
||
expect(result.updatedCards[0].skipCount).toBe(3) // 從2增加到3
|
||
expect(result.updatedCards[0].wrongCount).toBe(1) // 保持不變
|
||
})
|
||
})
|
||
```
|
||
|
||
### **測試2: 排序優先級邏輯**
|
||
```typescript
|
||
describe('延遲計數系統 - 排序邏輯', () => {
|
||
test('題目排序應該是被跳過次數越少越前面', () => {
|
||
// 準備不同延遲分數的卡片
|
||
const cards = [
|
||
{ id: '1', word: 'card1', skipCount: 3, wrongCount: 1, originalOrder: 1, isCompleted: false }, // 延遲分數: 4
|
||
{ id: '2', word: 'card2', skipCount: 1, wrongCount: 0, originalOrder: 2, isCompleted: false }, // 延遲分數: 1
|
||
{ id: '3', word: 'card3', skipCount: 0, wrongCount: 0, originalOrder: 3, isCompleted: false }, // 延遲分數: 0
|
||
{ id: '4', word: 'card4', skipCount: 2, wrongCount: 0, originalOrder: 4, isCompleted: false } // 延遲分數: 2
|
||
]
|
||
|
||
// 執行排序
|
||
const sorted = sortCardsByPriority(cards)
|
||
|
||
// 驗證排序結果 (延遲分數由小到大)
|
||
expect(sorted[0].id).toBe('3') // 延遲分數 0
|
||
expect(sorted[1].id).toBe('2') // 延遲分數 1
|
||
expect(sorted[2].id).toBe('4') // 延遲分數 2
|
||
expect(sorted[3].id).toBe('1') // 延遲分數 4
|
||
})
|
||
|
||
test('延遲分數相同時按原始順序排列', () => {
|
||
const cards = [
|
||
{ id: '1', skipCount: 1, wrongCount: 0, originalOrder: 3, isCompleted: false }, // 延遲分數: 1
|
||
{ id: '2', skipCount: 0, wrongCount: 1, originalOrder: 1, isCompleted: false }, // 延遲分數: 1
|
||
{ id: '3', skipCount: 1, wrongCount: 0, originalOrder: 2, isCompleted: false } // 延遲分數: 1
|
||
]
|
||
|
||
const sorted = sortCardsByPriority(cards)
|
||
|
||
// 相同延遲分數按originalOrder排序
|
||
expect(sorted[0].originalOrder).toBe(1)
|
||
expect(sorted[1].originalOrder).toBe(2)
|
||
expect(sorted[2].originalOrder).toBe(3)
|
||
})
|
||
})
|
||
```
|
||
|
||
### **測試3: 答錯效果等同跳過**
|
||
```typescript
|
||
describe('延遲計數系統 - 答錯處理', () => {
|
||
test('答錯的效果和跳過一樣都會被註記一次', () => {
|
||
const initialCard = {
|
||
id: '1',
|
||
word: 'test',
|
||
skipCount: 0,
|
||
wrongCount: 0,
|
||
isCompleted: false
|
||
}
|
||
|
||
// 測試跳過效果
|
||
const skippedResult = handleSkip([initialCard], 0)
|
||
const skippedScore = skippedResult.updatedCards[0].skipCount + skippedResult.updatedCards[0].wrongCount
|
||
|
||
// 測試答錯效果
|
||
const wrongResult = handleWrongAnswer([initialCard], 0)
|
||
const wrongScore = wrongResult.updatedCards[0].skipCount + wrongResult.updatedCards[0].wrongCount
|
||
|
||
// 兩者都應該增加1分延遲分數
|
||
expect(skippedScore).toBe(1)
|
||
expect(wrongScore).toBe(1)
|
||
})
|
||
|
||
test('信心度1-2算答錯,3算答對', () => {
|
||
const card = { id: '1', word: 'test', skipCount: 0, wrongCount: 0, isCompleted: false }
|
||
|
||
// 信心度1 (模糊) → 答錯
|
||
const result1 = handleConfidenceAnswer([card], 0, 1)
|
||
expect(result1.updatedCards[0].wrongCount).toBe(1)
|
||
expect(result1.updatedCards[0].isCompleted).toBe(false)
|
||
|
||
// 信心度2 (一般) → 答對
|
||
const result2 = handleConfidenceAnswer([card], 0, 2)
|
||
expect(result2.updatedCards[0].isCompleted).toBe(true)
|
||
expect(result2.updatedCards[0].wrongCount).toBe(0) // 不增加
|
||
|
||
// 信心度3 (熟悉) → 答對
|
||
const result3 = handleConfidenceAnswer([card], 0, 3)
|
||
expect(result3.updatedCards[0].isCompleted).toBe(true)
|
||
})
|
||
})
|
||
```
|
||
|
||
### **測試4: 排序不排除邏輯**
|
||
```typescript
|
||
describe('延遲計數系統 - 不排除原則', () => {
|
||
test('被跳過只是排序問題,不是直接排除', () => {
|
||
const cards = [
|
||
{ id: '1', word: 'high-delay', skipCount: 10, wrongCount: 5, isCompleted: false }, // 高延遲
|
||
{ id: '2', word: 'normal', skipCount: 0, wrongCount: 0, isCompleted: false }, // 正常
|
||
{ id: '3', word: 'completed', skipCount: 2, wrongCount: 1, isCompleted: true } // 已完成
|
||
]
|
||
|
||
const sorted = sortCardsByPriority(cards)
|
||
|
||
// 驗證沒有卡片被排除
|
||
expect(sorted).toHaveLength(3)
|
||
expect(sorted.find(c => c.id === '1')).toBeDefined() // 高延遲卡片仍存在
|
||
|
||
// 驗證排序正確
|
||
expect(sorted[0].id).toBe('2') // 正常卡片優先
|
||
expect(sorted[1].id).toBe('1') // 延遲卡片其次
|
||
expect(sorted[2].id).toBe('3') // 已完成卡片最後
|
||
})
|
||
|
||
test('即使跳過很多次的卡片仍會被練習', () => {
|
||
const cards = [
|
||
{ id: '1', word: 'difficult', skipCount: 20, wrongCount: 15, isCompleted: false }
|
||
]
|
||
|
||
const sorted = sortCardsByPriority(cards)
|
||
|
||
expect(sorted).toHaveLength(1)
|
||
expect(sorted[0].id).toBe('1') // 即使延遲分數很高,仍然存在
|
||
})
|
||
})
|
||
```
|
||
|
||
### **測試5: 完整流程集成**
|
||
```typescript
|
||
describe('延遲計數系統 - 完整流程', () => {
|
||
test('完整的學習會話流程', () => {
|
||
// 初始狀態: 3張卡片
|
||
let cards = [
|
||
{ id: '1', word: 'easy', skipCount: 0, wrongCount: 0, originalOrder: 1, isCompleted: false },
|
||
{ id: '2', word: 'medium', skipCount: 0, wrongCount: 0, originalOrder: 2, isCompleted: false },
|
||
{ id: '3', word: 'hard', skipCount: 0, wrongCount: 0, originalOrder: 3, isCompleted: false }
|
||
]
|
||
|
||
// 第1輪: easy答對, medium跳過, hard答錯
|
||
cards = handleConfidenceAnswer(cards, 0, 3).updatedCards // easy完成
|
||
cards = handleSkip(cards, 1).updatedCards // medium跳過+1
|
||
cards = handleWrongAnswer(cards, 2).updatedCards // hard答錯+1
|
||
|
||
// 排序後應該是: medium, hard (延遲分數都是1,按原順序)
|
||
const sorted1 = sortCardsByPriority(cards.filter(c => !c.isCompleted))
|
||
expect(sorted1[0].word).toBe('medium')
|
||
expect(sorted1[1].word).toBe('hard')
|
||
|
||
// 第2輪: medium再次跳過, hard答對完成
|
||
cards = handleSkip(cards, 1).updatedCards // medium跳過+1 (總共2次)
|
||
cards = handleConfidenceAnswer(cards, 2, 2).updatedCards // hard完成
|
||
|
||
// 最終只剩medium未完成,延遲分數為2
|
||
const remaining = cards.filter(c => !c.isCompleted)
|
||
expect(remaining).toHaveLength(1)
|
||
expect(remaining[0].word).toBe('medium')
|
||
expect(remaining[0].skipCount).toBe(2)
|
||
})
|
||
})
|
||
```
|
||
|
||
---
|
||
|
||
## 🎯 **測試實作函數**
|
||
|
||
### **需要實作的核心函數**
|
||
```typescript
|
||
// 這些是測試中使用的函數,需要在實際代碼中實作
|
||
|
||
interface TestResult {
|
||
updatedCards: CardState[]
|
||
nextIndex: number
|
||
}
|
||
|
||
// 跳過處理函數
|
||
function handleSkip(cards: CardState[], currentIndex: number): TestResult
|
||
|
||
// 答錯處理函數
|
||
function handleWrongAnswer(cards: CardState[], currentIndex: number): TestResult
|
||
|
||
// 信心度答題函數
|
||
function handleConfidenceAnswer(cards: CardState[], currentIndex: number, confidence: 1|2|3): TestResult
|
||
|
||
// 排序函數
|
||
function sortCardsByPriority(cards: CardState[]): CardState[]
|
||
```
|
||
|
||
---
|
||
|
||
## 📊 **測試覆蓋範圍**
|
||
|
||
### **功能覆蓋**
|
||
- ✅ 跳過次數計數
|
||
- ✅ 答錯次數計數
|
||
- ✅ 優先級排序邏輯
|
||
- ✅ 完成狀態管理
|
||
- ✅ 多次操作累計效果
|
||
|
||
### **邊界條件測試**
|
||
- ✅ 極高延遲分數的卡片
|
||
- ✅ 相同延遲分數的排序
|
||
- ✅ 全部完成的情況
|
||
- ✅ 單卡片情況
|
||
|
||
### **集成測試**
|
||
- ✅ 完整學習會話流程
|
||
- ✅ 多輪練習的狀態變化
|
||
- ✅ 不同信心度的處理
|
||
|
||
---
|
||
|
||
## 🚀 **TDD開發流程**
|
||
|
||
### **第1步: Red (測試失敗)**
|
||
```bash
|
||
npm test delay-counting-system.test.ts
|
||
# 所有測試應該失敗,因為函數還沒實作
|
||
```
|
||
|
||
### **第2步: Green (最小實作)**
|
||
```typescript
|
||
// 實作最簡單的版本讓測試通過
|
||
const handleSkip = (cards, index) => {
|
||
const newCards = [...cards]
|
||
newCards[index] = { ...newCards[index], skipCount: newCards[index].skipCount + 1 }
|
||
return { updatedCards: newCards, nextIndex: index + 1 }
|
||
}
|
||
```
|
||
|
||
### **第3步: Refactor (重構優化)**
|
||
```typescript
|
||
// 重構為更優雅的版本,保持測試通過
|
||
const handleSkip = useCallback((cards: CardState[], currentIndex: number) => {
|
||
const updatedCards = cards.map((card, index) =>
|
||
index === currentIndex
|
||
? { ...card, skipCount: card.skipCount + 1 }
|
||
: card
|
||
)
|
||
|
||
return {
|
||
updatedCards,
|
||
nextIndex: getNextIndex(updatedCards, currentIndex)
|
||
}
|
||
}, [])
|
||
```
|
||
|
||
---
|
||
|
||
## 🎯 **測試的業務價值**
|
||
|
||
### **確保需求實現**
|
||
- ✅ 您的4個核心需求都有對應測試
|
||
- ✅ 邊界情況都有覆蓋
|
||
- ✅ 集成場景完整測試
|
||
|
||
### **回歸保護**
|
||
- ✅ 未來修改不會破壞核心邏輯
|
||
- ✅ 重構時有安全保障
|
||
- ✅ 新功能不會影響現有行為
|
||
|
||
### **需求文檔化**
|
||
- ✅ 測試即是可執行的需求規格
|
||
- ✅ 開發者可以直接理解期望行為
|
||
- ✅ 產品經理可以驗證實作正確性
|
||
|
||
---
|
||
|
||
## 📋 **測試執行清單**
|
||
|
||
### **開發前**
|
||
- [ ] 所有測試都失敗 (因為還沒實作)
|
||
- [ ] 測試準確描述了您的需求
|
||
|
||
### **開發中**
|
||
- [ ] 逐個實作函數讓測試通過
|
||
- [ ] 保持測試綠燈狀態
|
||
- [ ] 重構時確保測試不破壞
|
||
|
||
### **完成後**
|
||
- [ ] 所有測試通過
|
||
- [ ] 覆蓋率達到90%+
|
||
- [ ] 邊界條件都正確處理
|
||
|
||
---
|
||
|
||
**這些測試完美地捕捉了您的延遲計數需求,可以指導TDD開發,確保實作完全符合您的預期!** 🎯
|
||
|
||
*測試規格制定: 2025-10-03*
|
||
*基於用戶明確需求*
|
||
*TDD開發就緒* |