iT邦幫忙

2025 iThome 鐵人賽

DAY 13
0
Software Development

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

Day 13:Laravel Pest TDD 實戰 - 擴展到百位數(11-100)

  • 分享至 

  • xImage
  •  

在 Day 12 我們成功建立了處理 1-10 的羅馬數字轉換器。今天我們要用 TDD 來擴展這個實作,讓它能夠處理更大的數字範圍,包括 11-100。

本日學習地圖 🗺️

開始 → 測試 11-19 → 處理 20-39 → 挑戰 40 (XL)
  ↓
完成 ← 模式總結 ← 處理 90-100 ← 引入 50 (L)

發現模式:處理 11-19

讓我們從測試 11 開始,看看現有實作是否能處理。這是一個重要的實驗,因為它會告訴我們現有演算法的威力。

建立 tests/Unit/Day13/RomanNumeralExtendedTest.php

<?php

use App\Roman\RomanNumeral;

test('converts 11 to XI', function () {
    $converter = new RomanNumeral();
    expect($converter->convert(11))->toBe('XI');
});

test('converts 19 to XIX', function () {
    $converter = new RomanNumeral();
    expect($converter->convert(19))->toBe('XIX');
});

執行測試:

php artisan test --filter=RomanNumeralExtendedTest

太好了!測試通過了。我們的查找表實作已經能正確處理 11-19 的範圍!

這表示我們之前設計的 while 迴圈策略非常正確,能夠自動處理重複的符號。這就是好的設計的價值!

TDD 實戰:處理 20-39

現在讓我們測試 20 和 27:

更新 tests/Unit/Day13/RomanNumeralExtendedTest.php

test('converts 20 to XX', function () {
    $converter = new RomanNumeral();
    expect($converter->convert(20))->toBe('XX');
});

test('converts 27 to XXVII', function () {
    $converter = new RomanNumeral();
    expect($converter->convert(27))->toBe('XXVII');
});

test('converts 30 to XXX', function () {
    $converter = new RomanNumeral();
    expect($converter->convert(30))->toBe('XXX');
});

test('converts 39 to XXXIX', function () {
    $converter = new RomanNumeral();
    expect($converter->convert(39))->toBe('XXXIX');
});

執行測試:

php artisan test --filter="converts (20|27|30|39)"

確認這些測試也通過!我們的 while 迴圈設計正確地處理了重複的符號。

為什麼這能運作?

這是因為我們的演算法設計:

  1. 貪婪策略:總是選擇最大的可用符號
  2. while 迴圈:重複使用相同符號直到不能再使用
  3. 順序處理:從大到小逐一處理

TDD 實戰:處理減法規則 40

現在我們遇到了第一個新的減法規則:40 = XL(50-10)。

這是一個很重要的測試案例,因為它會指出我們現有實作的不足。

test('converts 40 to XL', function () {
    $converter = new RomanNumeral();
    expect($converter->convert(40))->toBe('XL');
});

test('converts 44 to XLIV', function () {
    $converter = new RomanNumeral();
    expect($converter->convert(44))->toBe('XLIV');
});

執行測試...紅燈!我們需要添加這個減法規則到查找表中:

更新 app/Roman/RomanNumeral.php

private array $romanNumerals = [
    40 => 'XL',  // 重要:必須放在 10 之前
    10 => 'X',
    9 => 'IX', 
    5 => 'V',
    4 => 'IV',
    1 => 'I'
];

現在通過了!

設計原則的重要性

這裡有一個重要的設計原則:減法組合必須放在對應的基本符號之前。如果 XL(40) 放在 X(10) 之後,40 會被處理成 XXXX,而不是 XL。

TDD 實戰:處理 50 (L)

現在我們要處理新的符號 L = 50:

test('converts 50 to L', function () {
    $converter = new RomanNumeral();
    expect($converter->convert(50))->toBe('L');
});

test('converts 59 to LIX', function () {
    $converter = new RomanNumeral();
    expect($converter->convert(59))->toBe('LIX');
});

執行測試...紅燈!我們需要在查找表中添加 50。

更新 app/Roman/RomanNumeral.php

private array $romanNumerals = [
    50 => 'L',   // 新增 50
    40 => 'XL',
    10 => 'X',
    9 => 'IX', 
    5 => 'V',
    4 => 'IV',
    1 => 'I'
];

現在所有 50-59 的測試都通過了!

TDD 實戰:處理減法規則 90

讓我們跳到 90,這是另一個減法規則:90 = XC(100-10)。

這是另一個關鍵的測試點,因為它會測試我們的演算法是否能正確處理複雜的減法組合。

test('converts 90 to XC', function () {
    $converter = new RomanNumeral();
    expect($converter->convert(90))->toBe('XC');
});

test('converts 99 to XCIX', function () {
    $converter = new RomanNumeral();
    expect($converter->convert(99))->toBe('XCIX');
});

執行測試會失敗,因為目前會產生 "LXXXX" 而不是 "XC"。我們需要添加這個減法規則。

挑戰思考

在添加程式碼前,想想看:為什麼羅馬數字要使用減法規則?如果允許四個相同符號(如 XXXX),會有什麼問題?

TDD 實戰:完成 100

最後,讓我們處理 100 = C:

test('converts 100 to C', function () {
    $converter = new RomanNumeral();
    expect($converter->convert(100))->toBe('C');
});

發現模式:羅馬數字的結構

通過今天的擴展,我們發現了羅馬數字的重要模式:

1. 基礎符號

  • I(1), V(5), X(10), L(50), C(100)
  • 這些是羅馬數字系統的基本建築區塊

2. 減法規則

  • IV(4), IX(9), XL(40), XC(90)
  • 這些組合代表了「小數在前」的減法概念

3. 重複規則

  • II(2), III(3), XX(20), XXX(30)
  • 相同符號可以重複最多 3 次

我們的查找表方法非常適合這種模式,因為它:

  • 順序正確:按照從大到小的順序處理
  • 自動重複:自動處理重複(while 迴圈)
  • 減法優先:優先處理減法規則

完整測試套件

讓我們建立一個完整的測試套件來驗證我們的實作:

test('converts key numbers correctly', function () {
    $converter = new RomanNumeral();
    
    $testCases = [
        [11, 'XI'], [20, 'XX'], [27, 'XXVII'],
        [40, 'XL'], [44, 'XLIV'], [50, 'L'],
        [59, 'LIX'], [90, 'XC'], [99, 'XCIX'],
        [100, 'C']
    ];
    
    foreach ($testCases as [$number, $expected]) {
        expect($converter->convert($number))->toBe($expected);
    }
});

演算法分析

我們的實作使用貪婪演算法配合查找表:

  • 貪婪策略:每次選擇可能的最大值
  • 查找表:預先定義所有符號組合
  • 迴圈處理:重複使用符號直到無法使用

時間複雜度:O(1),空間複雜度:O(1)

完整實作

完整實作 app/Roman/RomanNumeral.php

<?php

namespace App\Roman;

class RomanNumeral
{
    private array $romanNumerals = [
        100 => 'C',   // 一百
        90  => 'XC',  // 九十(減法規則)
        50  => 'L',   // 五十
        40  => 'XL',  // 四十(減法規則)
        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;
    }
}

今天學到什麼

技術面

  1. 模式擴展:從 1-10 的模式成功擴展到 1-100
  2. 減法規則完整性:理解了 XL(40) 和 XC(90) 的減法規則
  3. 算法穩定性:我們的查找表方法很容易擴展
  4. 貪婪演算法:學會了使用貪婪法來解決羅馬數字轉換

TDD 面

  1. TDD 優勢:測試先行讓我們能夠安全地擴展功能
  2. 測試驅動設計:由測試失敗引導出正確的實作
  3. Pest 語法:更加熟練 Pest 的測試語法

成功達成目標

我們現在擁有一個優雅且可靠的羅馬數字轉換器!通過今天的實作,我們看到了 TDD 如何幫助我們系統性地擴展功能。

TDD 的三色燈過程

  • 紅燈:指出了我們需要實作的具體功能
  • 綠燈:確認了我們的實作正確性
  • 重構:讓我們的代碼保持 clean 和可維護

上一篇
Day 12 - 基礎符號轉換(1-10) 🔢
下一篇
Day 14:Laravel Pest TDD 實戰 - 完整範圍實作(1-3999)🏛️
系列文
Laravel Pest TDD 實戰:從零開始的測試驅動開發14
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言