iT邦幫忙

2025 iThome 鐵人賽

DAY 17
0
Modern Web

React TDD 實戰:用 Vitest 打造可靠的前端應用系列 第 17

Day 17 - 程式碼整理與回顧 🏁

  • 分享至 

  • xImage
  •  

完成了六天的羅馬數字轉換器開發,今天我們要進行程式碼的最終整理與回顧。將散落在不同測試日期中的程式碼重構成一個乾淨、可維護、生產就緒的模組。

今天的目標 🎯

  1. 整理和重構完整的程式碼
  2. 建立清晰的模組結構
  3. 加入完整的 TypeScript 類型定義
  4. 建立綜合測試套件
  5. 回顧 TDD 學習歷程
  6. ✅ 建立乾淨、可維護的最終版本

羅馬數字轉換器回顧 📚

在過去 6 天的開發歷程中,我們從最簡單的測試 toRoman(1) → "I" 開始,逐步建立了完整的羅馬數字轉換器。

建立生產就緒的模組結構 🏗️

完整模組實作

建立統一的模組實作:

// 建立 src/roman/index.ts
export interface RomanConverter {
  toRoman: (num: number) => string
  fromRoman: (roman: string) => number
}

// 基本符號對應表
const ROMAN_MAPPINGS: ReadonlyArray<readonly [number, string]> = [
  [1000, 'M'], [900, 'CM'], [500, 'D'], [400, 'CD'],
  [100, 'C'], [90, 'XC'], [50, 'L'], [40, 'XL'],
  [10, 'X'], [9, 'IX'], [5, 'V'], [4, 'IV'], [1, 'I']
] as const

const ROMAN_VALUES: Record<string, number> = {
  'M': 1000, 'CM': 900, 'D': 500, 'CD': 400,
  'C': 100, 'XC': 90, 'L': 50, 'XL': 40,
  'X': 10, 'IX': 9, 'V': 5, 'IV': 4, 'I': 1
} as const

export function toRoman(num: number): string {
  if (!Number.isInteger(num) || num <= 0 || num > 3999) {
    throw new Error('Number must be an integer between 1 and 3999')
  }

  let result = ''
  let remaining = num

  for (const [value, symbol] of ROMAN_MAPPINGS) {
    const count = Math.floor(remaining / value)
    if (count > 0) {
      result += symbol.repeat(count)
      remaining -= value * count
    }
  }
  return result
}

export function fromRoman(roman: string): number {
  if (!roman || typeof roman !== 'string') {
    throw new Error('Invalid Roman numeral: empty or non-string input')
  }

  const normalizedRoman = roman.trim().toUpperCase()
  const validChars = new Set(['I', 'V', 'X', 'L', 'C', 'D', 'M'])
  for (const char of normalizedRoman) {
    if (!validChars.has(char)) {
      throw new Error(`Invalid Roman character: '${char}'`)
    }
  }

  let result = 0
  let i = 0

  while (i < normalizedRoman.length) {
    const twoChar = normalizedRoman.substring(i, i + 2)
    const oneChar = normalizedRoman[i]

    if (ROMAN_VALUES[twoChar]) {
      result += ROMAN_VALUES[twoChar]
      i += 2
    } else if (ROMAN_VALUES[oneChar]) {
      result += ROMAN_VALUES[oneChar]
      i += 1
    }
  }
  return result
}

export function isValidRoman(roman: string): boolean {
  try {
    const num = fromRoman(roman)
    return toRoman(num) === roman.trim().toUpperCase()
  } catch {
    return false
  }
}

const romanConverter: RomanConverter = { toRoman, fromRoman }
export default romanConverter

綜合測試套件 🧪

建立完整的測試套件:

// 建立 tests/day17/roman-converter.test.ts
import { describe, test, expect } from 'vitest'
import { toRoman, fromRoman, isValidRoman } from '../../src/roman/index'

describe('Roman Converter - Production Tests', () => {
  test('converts basic numbers correctly', () => {
    expect(toRoman(1)).toBe('I')
    expect(toRoman(4)).toBe('IV')
    expect(toRoman(1994)).toBe('MCMXCIV')
  })

  test('converts Roman numerals correctly', () => {
    expect(fromRoman('I')).toBe(1)
    expect(fromRoman('IV')).toBe(4)
    expect(fromRoman('MCMXCIV')).toBe(1994)
  })

  test('bidirectional conversion consistency', () => {
    const testNumbers = [1, 4, 5, 9, 10, 40, 50, 90, 100, 400, 500, 900, 1000, 1994]
    
    for (const num of testNumbers) {
      const roman = toRoman(num)
      expect(fromRoman(roman)).toBe(num)
    }
  })

  test('validates Roman numerals', () => {
    expect(isValidRoman('IV')).toBe(true)
    expect(isValidRoman('IIII')).toBe(false)
  })

  test('handles error cases', () => {
    expect(() => toRoman(0)).toThrow()
    expect(() => fromRoman('ABC')).toThrow()
  })
})

TDD 學習回顧與反思 💡

透過六天的 Roman Numeral Kata,我們學到了什麼?

TDD 循環的威力

紅-綠-重構 循環讓我們始終保持程式碼有測試覆蓋,每次只專注解決一個問題,透過重構持續改善設計,建立可靠的安全網。

漸進式開發的重要性

toRoman(1) → "I" 開始,我們學會了先讓最簡單的測試通過,逐步增加複雜性,每次變更都有測試保護。

重構的信心

有了測試作為安全網,我們能夠放心改善程式碼結構,優化效能而不破壞功能,重新組織模組架構。

完整實作總結 📋

核心功能

  • 阿拉伯數字轉羅馬數字 (toRoman)
  • 羅馬數字轉阿拉伯數字 (fromRoman)
  • 羅馬數字格式驗證 (isValidRoman)

品質特性

  • TypeScript 類型支援、錯誤處理、高效能實作

測試執行與驗證 ✅

執行完整測試

npm test tests/day17/
npm test tests/day17/ --coverage

品質檢查

npx tsc --noEmit
npx eslint src/roman/

除錯技巧

在開發過程中的常見問題:

// 類型錯誤處理
const num = parseInt(input, 10)
if (!isNaN(num)) toRoman(num)

// 邊界值測試
expect(() => toRoman(0)).toThrow('between 1 and 3999')

TDD 深度反思與學習心得 🤔

六天開發歷程回顧

從Day 11到Day 16的TDD實踐:

  • Day 11: 第一個測試 toRoman(1) → "I"
  • Day 12: 處理重複數字與循環邏輯
  • Day 13: 減法規則(IV, IX)與測試驅動設計
  • Day 14: 完整範圍支援與錯誤處理
  • Day 15: fromRoman雙向轉換功能
  • Day 16: 效能優化策略

TDD 實踐技能深化 🎯

紅-綠-重構循環的內化

經過六天實作,我們深刻理解:

  • 紅階段:寫失敗測試,明確表達期望
  • 綠階段:最快速讓測試通過
  • 重構階段:在測試保護下改善程式碼

測試驅動的API設計

測試優先的設計理念:

interface RomanConverter {
  toRoman: (num: number) => string
  fromRoman: (roman: string) => number
}

漸進式開發的威力

從最簡單開始:單一數字→重複數字→減法規則→完整範圍→雙向轉換→效能優化。每一步都有測試保護。

TypeScript生態系統整合 🔧

type RomanNumeral = string & { __brand: 'roman' }
import { describe, test, expect } from 'vitest'

測試策略演進

透過六天的實作,我們的測試策略也在不斷進化:

測試組織方式的改進

// Day 11 - 簡單的單一測試
test('converts 1 to I', () => {
  expect(toRoman(1)).toBe('I')
})

// Day 17 - 組織化的測試套件
describe('Roman Converter', () => {
  describe('toRoman', () => {
    test('handles single digits', () => { /* ... */ })
    test('handles subtraction cases', () => { /* ... */ })
  })
})

測試覆蓋率的提升

  • 從單一案例到全面覆蓋
  • 從正向測試到包含錯誤處理
  • 從單向轉換到雙向驗證

今天的核心收穫 🎁

TDD 實踐技能

  • 熟練掌握紅-綠-重構循環
  • 學會寫出清晰的測試案例
  • 建立測試優先的開發習慣
  • 理解測試驅動的API設計

TypeScript 程式設計技能

  • 模組化設計思維
  • 完整的錯誤處理策略
  • TypeScript 類型系統運用
  • 現代JavaScript/TypeScript最佳實踐

React 生態系統整合

  • Vitest測試框架熟練應用
  • 現代前端工程化實踐

軟體工程實踐

  • 漸進式開發方法
  • 測試覆蓋策略
  • 函數介面設計原則

旅程地圖回顧 🗺️

讓我們回顧這 17 天的學習之旅:

第一階段回顧(Day 1-10)

  • ✅ 環境設定與 Vitest 框架初探
  • ✅ 斷言方法與測試結構
  • ✅ 紅綠重構循環實踐
  • ✅ 測試生命週期掌握
  • ✅ 參數化測試技巧
  • ✅ 測試替身運用
  • ✅ 例外處理測試
  • ✅ 測試覆蓋率分析
  • ✅ 重構技巧深化

第二階段完成(Day 11-17)

  • ✅ 測試設計模式導入
  • ✅ 基礎符號轉換
  • ✅ 百位數處理擴展
  • ✅ 完整範圍實作
  • ✅ 雙向轉換功能
  • ✅ 效能優化策略
  • ✅ 程式碼整理回顧

總結與鼓勵 💪

完成 Roman Numeral Kata 是一個重要的里程碑!我們不僅學會了 TDD 的核心技術,更建立了以測試驅動開發的思維模式。

每一個測試都是對品質的承諾,每一次重構都是對卓越的追求。讓我們帶著這些收穫,繼續前進!


上一篇
Day 16 - 效能優化 🚀
下一篇
React Testing Library 入門 🌐
系列文
React TDD 實戰:用 Vitest 打造可靠的前端應用20
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言