iT邦幫忙

2025 iThome 鐵人賽

DAY 12
0
Software Development

Laravel Pest TDD 實戰:從零開始的測試驅動開發系列 第 12

Day 12 - 基礎符號轉換(1-10) 🔢

  • 分享至 

  • xImage
  •  

昨天成功處理了 1、2、3,但 4 輸出 "IIII" 而非 "IV"。今天用 TDD 處理羅馬數字的減法規則。

羅馬數字減法規則 ⚖️

核心規則

  • 小數字在大數字前面時做減法
  • IV = V - I = 5 - 1 = 4
  • IX = X - I = 10 - 1 = 9

處理數字 4:第一個減法規則 🎯

建立 tests/Unit/Day12/RomanNumeralTest.php

<?php

use App\Roman\RomanNumeral;

test('converts 1 to I', fn() => expect((new RomanNumeral())->convert(1))->toBe('I'));
test('converts 2 to II', fn() => expect((new RomanNumeral())->convert(2))->toBe('II'));
test('converts 3 to III', fn() => expect((new RomanNumeral())->convert(3))->toBe('III'));
test('converts 4 to IV', fn() => expect((new RomanNumeral())->convert(4))->toBe('IV'));

測試失敗 🔴:Expected: "IV", Received: "IIII"

更新 app/Roman/RomanNumeral.php

public function convert(int $number): string
{
    if ($number === 4) return 'IV';
    
    $result = '';
    for ($i = 0; $i < $number; $i++) {
        $result .= 'I';
    }
    return $result;
}

測試通過! 🟢

處理數字 5:V 的登場 ✨

加入測試:

test('converts 5 to V', fn() => expect((new RomanNumeral())->convert(5))->toBe('V'));

修改實作:

public function convert(int $number): string
{
    if ($number === 5) return 'V';
    if ($number === 4) return 'IV';
    
    $result = '';
    for ($i = 0; $i < $number; $i++) {
        $result .= 'I';
    }
    return $result;
}

處理數字 6-8:V 系列測試 📈

加入測試:

test('converts 6 to VI', fn() => expect((new RomanNumeral())->convert(6))->toBe('VI'));
test('converts 7 to VII', fn() => expect((new RomanNumeral())->convert(7))->toBe('VII'));
test('converts 8 to VIII', fn() => expect((new RomanNumeral())->convert(8))->toBe('VIII'));

重構 實作:

public function convert(int $number): string
{
    if ($number >= 5) {
        return 'V' . $this->convertOnes($number - 5);
    }
    if ($number === 4) return 'IV';
    return $this->convertOnes($number);
}

private function convertOnes(int $number): string
{
    $result = '';
    for ($i = 0; $i < $number; $i++) {
        $result .= 'I';
    }
    return $result;
}

挑戰數字 9:第二個減法規則 🎯

加入測試:

test('converts 9 to IX', fn() => expect((new RomanNumeral())->convert(9))->toBe('IX'));

測試失敗 🔴:輸出 "VIIII" 而非 "IX"

更新 實作:

public function convert(int $number): string
{
    if ($number === 9) return 'IX';
    if ($number >= 5) {
        return 'V' . $this->convertOnes($number - 5);
    }
    if ($number === 4) return 'IV';
    return $this->convertOnes($number);
}

處理數字 10 🔟

加入測試:

test('converts 10 to X', fn() => expect((new RomanNumeral())->convert(10))->toBe('X'));

更新 實作:

public function convert(int $number): string
{
    if ($number === 10) return 'X';
    if ($number === 9) return 'IX';
    if ($number >= 5) {
        return 'V' . $this->convertOnes($number - 5);
    }
    if ($number === 4) return 'IV';
    return $this->convertOnes($number);
}

測試通過! 🟢

最終重構:映射表模式 🎨

重構 成映射表形式:

<?php

namespace App\Roman;

class RomanNumeral
{
    private array $romanNumerals = [
        10 => 'X', 9 => 'IX', 5 => 'V', 4 => 'IV', 1 => 'I'
    ];
    
    public function convert(int $number): string
    {
        $result = '';
        
        foreach ($this->romanNumerals as $value => $numeral) {
            while ($number >= $value) {
                $result .= $numeral;
                $number -= $value;
            }
        }
        
        return $result;
    }
}

測試通過! ✅

TDD 學習重點 💡

透過 TDD,我們發現羅馬數字的重要模式:

  • 大數字優先:先處理大數字(10 → 5 → 1)
  • 減法規則優先:4 和 9 要特別處理
  • 貪婪算法:每次選擇最大可能符號

完整程式碼 📋

完整測試 tests/Unit/Day12/RomanNumeralTest.php

<?php

use App\Roman\RomanNumeral;

// 測試 1-5
test('converts 1 to I', fn() => expect((new RomanNumeral())->convert(1))->toBe('I'));
test('converts 2 to II', fn() => expect((new RomanNumeral())->convert(2))->toBe('II'));
test('converts 3 to III', fn() => expect((new RomanNumeral())->convert(3))->toBe('III'));
test('converts 4 to IV', fn() => expect((new RomanNumeral())->convert(4))->toBe('IV'));
test('converts 5 to V', fn() => expect((new RomanNumeral())->convert(5))->toBe('V'));

// 測試 6-10
test('converts 6 to VI', fn() => expect((new RomanNumeral())->convert(6))->toBe('VI'));
test('converts 7 to VII', fn() => expect((new RomanNumeral())->convert(7))->toBe('VII'));
test('converts 8 to VIII', fn() => expect((new RomanNumeral())->convert(8))->toBe('VIII'));
test('converts 9 to IX', fn() => expect((new RomanNumeral())->convert(9))->toBe('IX'));
test('converts 10 to X', fn() => expect((new RomanNumeral())->convert(10))->toBe('X'));

完整實作 app/Roman/RomanNumeral.php

<?php

namespace App\Roman;

class RomanNumeral
{
    private array $romanNumerals = [
        10 => 'X', 9 => 'IX', 5 => 'V', 4 => 'IV', 1 => 'I'
    ];
    
    public function convert(int $number): string
    {
        $result = '';
        
        foreach ($this->romanNumerals as $value => $numeral) {
            while ($number >= $value) {
                $result .= $numeral;
                $number -= $value;
            }
        }
        
        return $result;
    }
}

測試驗證 ✅

執行測試:

./vendor/bin/pest tests/Unit/Day12/RomanNumeralTest.php

所有測試通過!我們有了處理 1-10 的羅馬數字轉換器。

今天學到的重點 📝

  • 減法規則:IV = 4, IX = 9
  • TDD 循環:紅-綠-重構節奏
  • 映射表模式:簡化條件邏輯
  • 大數字優先:貪婪算法概念
  • 測試驅動設計:讓測試指導實作

今日小挑戰 🏆

試著思考以下問題:

  1. 為什麼選擇映射表模式?有什麼優點?
  2. 如果要處理 0 或負數,該如何修改?
  3. 映射表的順序為什麼重要?

重點回顧 🎁

今天我們成功實作了 1-10 的羅馬數字轉換,從最簡單的 if-else 開始,逐步演進到優雅的映射表模式。這就是 TDD 的魅力 - 讓我們的程式碼隨著測試逐步進化。

每個測試都像是一個小小的里程碑,紅燈告訴我們方向,綠燈確認我們走對了路,重構讓我們的程式碼更加優雅。這就是 TDD 的節奏感!

今天的 TDD 之旅就到這裡,明天我們繼續深入探索! 🚀


上一篇
Day 11 - Kata 入門與環境準備
下一篇
Day 13:Laravel Pest TDD 實戰 - 擴展到百位數(11-100)
系列文
Laravel Pest TDD 實戰:從零開始的測試驅動開發14
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言