dramaling-vocab-learning/note/複習系統/延遲計數系統測試規格.md

11 KiB
Raw Blame History

延遲計數系統測試規格

目的: 將您的延遲計數需求轉換為可執行的測試案例 測試範圍: 跳過功能、排序邏輯、答錯處理、優先級管理 測試方式: Jest + React Testing Library 最後更新: 2025-10-03


🧪 核心需求測試

測試1: 跳過功能基礎邏輯

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: 排序優先級邏輯

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: 答錯效果等同跳過

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: 排序不排除邏輯

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: 完整流程集成

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)
  })
})

🎯 測試實作函數

需要實作的核心函數

// 這些是測試中使用的函數,需要在實際代碼中實作

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 (測試失敗)

npm test delay-counting-system.test.ts
# 所有測試應該失敗,因為函數還沒實作

第2步: Green (最小實作)

// 實作最簡單的版本讓測試通過
const handleSkip = (cards, index) => {
  const newCards = [...cards]
  newCards[index] = { ...newCards[index], skipCount: newCards[index].skipCount + 1 }
  return { updatedCards: newCards, nextIndex: index + 1 }
}

第3步: Refactor (重構優化)

// 重構為更優雅的版本,保持測試通過
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開發就緒