iT邦幫忙

2025 iThome 鐵人賽

DAY 10
0
Software Development

Python pytest TDD 實戰:從零開始的測試驅動開發系列 第 11

Day 10 - 重構與測試:讓程式碼持續進化 🔧

  • 分享至 

  • xImage
  •  

還記得第一次接手別人寫的程式碼嗎?那種「這是什麼?」的困惑、「為什麼要這樣寫?」的疑問,以及「我該從哪裡開始改?」的無助感。每個開發者都有過這樣的經歷。

經過前九天的學習,我們掌握了 TDD 的基本工具。今天來學習「重構與測試」:在不改變程式外部行為的前提下,改善程式碼內部結構。這就像整理房間一樣,外觀看起來還是同一個房間,但內部變得井然有序、使用起來更方便。

有了測試作為安全網,重構就變得安全而有信心。測試會告訴你:「重構是否保持了原有的行為」。這就像走鋼索時下面有安全網,讓你可以大膽前進。

今日學習地圖 🗺️

重構之旅啟程!
├── 第一站:理解重構的本質
├── 第二站:測試驅動的重構實戰
├── 第三站:掌握重構技巧
├── 第四站:重構最佳實踐
└── 終點站:10天學習總結與回顧

學習目標 🎯

今天你將學會:

  • 理解重構的概念和重要性
  • 掌握常見的重構技巧
  • 學會在測試保護下進行安全重構
  • 總結前 10 天的 TDD 學習成果

什麼是重構?🔄

重構是透過小步驟改善程式碼結構,同時保持程式的外部行為不變。Martin Fowler 在《Refactoring》一書中說:「重構是在不改變軟體可觀察行為的前提下,改善其內部結構」。

重構 vs 重寫

很多人常把重構和重寫搞混:

特性 重構 重寫
改變外部行為 ❌ 否 ✅ 可能
需要測試保護 ✅ 必須 ⚠️ 不一定
風險程度
進行方式 小步驟 大範圍
時間投入 持續進行 一次性

為什麼要重構? 💡

  1. 提升可讀性:讓程式碼更容易理解

    • 有意義的命名
    • 清晰的結構
    • 適當的抽象層次
  2. 減少重複:遵循 DRY (Don't Repeat Yourself) 原則

    • 消除複製貼上的程式碼
    • 提取共用邏輯
    • 建立可重用元件
  3. 提升維護性:修改和擴展更容易

    • 降低修改成本
    • 減少出錯機會
    • 加快開發速度
  4. 降低複雜度:簡化複雜的邏輯

    • 分解大函數
    • 簡化條件判斷
    • 改善資料結構

何時該重構? ⏰

三法則(Rule of Three):

  1. 第一次做某件事時,直接做
  2. 第二次做類似的事時,會有點不情願但還是做了
  3. 第三次做類似的事時,就該重構了

測試驅動的重構 🚀

重構的黃金法則:在重構之前,你必須有穩固的測試。沒有測試的重構是危險的,就像沒有安全帶就開車一樣。

重構的安全步驟

1. 確認測試都是綠燈 ✅
2. 執行小步驟重構 🔧
3. 執行測試驗證 🧪
4. 如果測試失敗,立即回復 ↩️
5. 重複直到完成 🔄

讓我們透過實際案例來體驗重構的過程:

重構前後對比範例

建立 src/day10/calculator.py

# 重構前:複雜的計算器
class Calculator:
    def calculate(self, a: float, b: float, operation: str) -> float:
        if operation == 'add':
            return a + b
        elif operation == 'subtract':
            return a - b
        elif operation == 'multiply':
            return a * b
        elif operation == 'divide':
            if b == 0:
                raise ValueError('Cannot divide by zero')
            return a / b
        else:
            raise ValueError('Unknown operation')

建立 tests/day10/test_calculator.py

import pytest
from src.day10.calculator import Calculator

class TestCalculator:
    def setup_method(self):
        self.calc = Calculator()

    def test_addition(self):
        assert self.calc.calculate(5.0, 3.0, 'add') == 8.0

    def test_subtraction(self):
        assert self.calc.calculate(5.0, 3.0, 'subtract') == 2.0

    def test_multiplication(self):
        assert self.calc.calculate(5.0, 3.0, 'multiply') == 15.0

    def test_division(self):
        assert self.calc.calculate(6.0, 2.0, 'divide') == 3.0

    def test_division_by_zero_raises_error(self):
        with pytest.raises(ValueError, match='Cannot divide by zero'):
            self.calc.calculate(5.0, 0.0, 'divide')

    def test_unknown_operation_raises_error(self):
        with pytest.raises(ValueError, match='Unknown operation'):
            self.calc.calculate(5.0, 3.0, 'unknown')

執行重構 ⚡

現在我們有了測試安全網,可以安心進行重構:

更新 src/day10/calculator.py

# 重構後:使用字典策略模式
class Calculator:
    def calculate(self, a: float, b: float, operation: str) -> float:
        operations = {
            'add': self._add,
            'subtract': self._subtract,
            'multiply': self._multiply,
            'divide': self._divide
        }
        
        if operation not in operations:
            raise ValueError('Unknown operation')
        
        return operations[operation](a, b)
    
    def _add(self, a: float, b: float) -> float:
        return a + b
    
    def _subtract(self, a: float, b: float) -> float:
        return a - b
    
    def _multiply(self, a: float, b: float) -> float:
        return a * b
    
    def _divide(self, a: float, b: float) -> float:
        if b == 0:
            raise ValueError('Cannot divide by zero')
        return a / b

常用重構技巧 🎯

1. 提取方法(Extract Method)

當一個方法太長或做太多事情時,可以提取出語義清晰的小方法。

2. 消除重複(Remove Duplication)

當發現重複的程式碼時,應該提取共同邏輯,遵循 DRY 原則。

3. 簡化條件(Simplify Conditionals)

複雜的條件判斷應該被簡化或提取成有意義的方法。

重構的安全原則 🛡️

  1. 小步驟重構:一次只改一個地方,降低風險
  2. 持續測試:每次重構後都執行測試,確保功能正確
  3. 保持功能不變:重構不改變外部行為和函數介面
  4. 改善內部結構:讓程式碼更易讀、易維護、易擴展
  5. 版本控制:每次重構都要提交,方便回滾
  6. 團隊溝通:重構前與團隊成員溝通,避免衝突

重構實戰演練 🎪

將前面學到的技巧應用到訂單處理服務中:

建立 src/day10/order_service.py

# 重構後:職責分離,邏輯清晰
class OrderService:
    def process_order(self, order_data: dict) -> dict:
        self._validate_order(order_data)
        subtotal = self._calculate_subtotal(order_data['items'])
        discount = self._calculate_discount(subtotal, order_data.get('customer_type'))
        
        return {
            'order_id': f"ORDER_{order_data['customer_id']}",
            'subtotal': subtotal,
            'discount': discount,
            'total': subtotal - discount
        }
    
    def _validate_order(self, order_data: dict) -> None:
        if not order_data.get('customer_id'):
            raise ValueError('Customer ID is required')
        if not order_data.get('items'):
            raise ValueError('Order must have at least one item')
    
    def _calculate_subtotal(self, items: list) -> float:
        return sum(item['price'] * item['quantity'] for item in items)
    
    def _calculate_discount(self, subtotal: float, customer_type: str) -> float:
        if customer_type == 'VIP':
            return subtotal * 0.1
        elif subtotal > 1000:
            return subtotal * 0.05
        return 0.0

重構的最佳實踐 ✨

1. 小步驟重構

每次只重構一小部分,執行測試確保功能正常,提交版本控制,逐步改善程式碼結構。

2. 每次重構後都執行測試

$ pytest tests/day10/  # ✅ 確認測試綠燈
$ # 執行重構...
$ pytest tests/day10/  # 🧪 驗證行為未變
$ git commit -m "refactor: extract method for validation"

3. 重構的時機選擇

適合重構:新增功能前、修復 bug 時、Code Review 時
不適合重構:接近截止日期、沒有測試保護時

建立 tests/day10/test_order_service.py

import pytest
from src.day10.order_service import OrderService

class TestOrderService:
    def setup_method(self):
        self.service = OrderService()
    
    def test_calculate_regular_order(self):
        order = {
            'customer_id': 'C123',
            'customer_type': 'Regular',
            'items': [{'price': 100, 'quantity': 2}]
        }
        result = self.service.process_order(order)
        assert result['total'] == 200
    
    def test_calculate_vip_discount(self):
        order = {
            'customer_id': 'V456',
            'customer_type': 'VIP',
            'items': [{'price': 500, 'quantity': 2}]
        }
        result = self.service.process_order(order)
        assert result['discount'] == 100  # 10% VIP discount

第一階段總結:10 天 TDD 基礎之旅 🎓

恭喜你!完成了 TDD 第一階段的學習。讓我們回顧這 10 天的精彩旅程:

學習軌跡回顧 📈

Day 01-03:奠定基礎
├── Day 01:環境設定與第一個測試
├── Day 02:理解斷言的藝術
└── Day 03:TDD 紅綠重構循環

Day 04-06:深化理解
├── Day 04:測試結構與組織
├── Day 05:測試生命週期
└── Day 06:參數化測試的威力

Day 07-09:測試技巧
├── Day 07:測試替身基礎
├── Day 08:例外處理測試
└── Day 09:測試覆蓋率分析

Day 10:整合提升
└── 重構與測試的完美搭配

技能成長地圖 🗺️

階段 核心技能 實戰能力
入門 (Day 1-3) ✅ pytest 測試框架✅ 基本斷言✅ 紅綠重構 能寫簡單的單元測試
基礎 (Day 4-6) ✅ 測試組織✅ 生命週期✅ 資料驅動測試 能組織大型測試套件
深化 (Day 7-9) ✅ Mock/Stub✅ 例外測試✅ 覆蓋率分析 能測試複雜場景
整合 (Day 10) ✅ 安全重構✅ 程式碼品質✅ 持續改進 能維護高品質程式碼

關鍵收穫總結 💎

  1. 測試優先思維:先寫測試,後寫程式碼
  2. 小步驟開發:每次只改一點點,保持綠燈
  3. 重構信心:有測試保護,重構不再恐懼
  4. 品質意識:測試不只是找 bug,更是設計工具

你已經掌握的能力 ⚡

✅ 能夠設定 Python pytest 測試環境
✅ 熟練使用各種斷言方法
✅ 理解並實踐 TDD 紅綠重構循環
✅ 能夠組織和管理測試程式碼
✅ 掌握測試生命週期鉤子
✅ 會使用參數化測試減少重複
✅ 能夠創建和使用測試替身
✅ 知道如何測試例外情況
✅ 理解測試覆蓋率的意義
✅ 能在測試保護下安全重構

第一階段的基礎訓練圓滿完成!🎉

總結 🎊

今天我們學會了測試驅動的重構,這是 TDD 循環中「重構」步驟的深入實踐。有了測試作為安全網,我們可以大膽地改善程式碼結構,讓系統變得更好。

重構不是一次性的大工程,而是持續的小改進。就像園丁修剪花園,每天做一點,最終會有一個美麗的花園。

重要心法

「寫程式」是為了讓機器理解
「重構」是為了讓人類理解
「測試」是為了讓改變安全

第一階段的學習到此圓滿結束!記住 TDD 的精髓:紅 → 綠 → 重構。測試不只是為了找 bug,更是設計工具和重構的安全網!

恭喜你完成了 TDD 基礎學習的前十天!繼續努力,你將能掌握更多 TDD 技巧!🚀


上一篇
Day 09 - 測試覆蓋率:你的測試真的夠完整嗎? 📊
系列文
Python pytest TDD 實戰:從零開始的測試驅動開發11
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言