iT邦幫忙

2025 iThome 鐵人賽

DAY 4
0

今天要做什麼?

昨天我們學會了 TDD 的紅綠重構循環,體驗了從無到有開發功能的完整流程。隨著測試越寫越多,你可能開始感到困擾:「這些測試散落各處,很難找到我要的測試」、「類似的測試重複出現,但又不完全一樣」。

想像一個場景:你的數學工具庫現在有 20 個函數,每個函數有 5-8 個測試案例,總共 100 多個測試。當某個測試失敗時,你需要在一大堆 test_ 函數中找到問題所在,這時你就會深刻體會到「測試結構」的重要性。

今天我們要學習如何組織測試,讓測試代碼變得清晰、有條理,就像整理房間一樣 —— 相關的東西放在一起,每樣東西都有固定位置。

學習目標

今天結束後,你將學會:

  • 掌握測試檔案的組織結構
  • 理解測試套件(Test Suite)的概念
  • 學會使用類別和函數組織測試
  • 掌握測試命名的最佳實踐
  • 學會將測試分類和分組

TDD 學習地圖

第一階段:打好基礎(Day 1-10)
├── Day 01 - 環境設置與第一個測試
├── Day 02 - 認識斷言(Assertions)
├── Day 03 - TDD 紅綠重構循環
├── Day 04 - 測試結構與組織 ★ 今天在這裡
├── ...
└── (更多精彩內容待續)

為什麼需要測試結構? 🧮

散亂測試的痛點

回想昨天的數學工具庫,如果我們繼續用扁平的方式寫測試:

# 混亂的測試結構
def test_is_prime_with_2_should_return_true():
def test_is_prime_with_3_should_return_true():
def test_is_prime_with_4_should_return_false():
def test_count_primes_in_range_with_range_1_10():
def test_is_prime_with_negative_number_should_return_false():
def test_fibonacci_with_0_should_return_0():

問題:

  • 找不到相關測試is_prime 的測試散落各處
  • 缺乏邏輯分組:正向測試、邊界測試混在一起
  • 測試命名冗長:每個測試都要重複函數名
  • 難以維護:新增測試不知道放哪裡

結構化測試的好處

# 結構化的測試組織
class TestMathUtils:
    class TestIsPrime:
        class TestPrimeDetection:
            def test_identify_small_primes(self):
            def test_identify_large_primes(self):
        
        class TestCompositeDetection:
            def test_identify_small_composites(self):
            def test_identify_large_composites(self):
        
        class TestBoundaryConditions:
            def test_handle_negative_numbers(self):
            def test_handle_zero_and_one(self):

好處:

  • 邏輯清晰:相關測試聚在一起
  • 階層分明:從大到小,從抽象到具體
  • 命名簡潔:測試名稱更精確
  • 易於維護:新測試有明確的歸屬

測試套件(Test Suite)概念 📝

測試套件是一組相關測試的集合,用來驗證特定功能或模組。在 pytest 中,我們可以用類別來創建測試套件:

class TestFunctionName:
    """這個類別裡的所有測試都屬於這個套件"""
    
    def test_case_1(self):
        pass
    
    def test_case_2(self):
        pass

測試套件的階層結構

class TestMathUtils:              # 頂層套件:模組
    class TestIsPrime:            # 第二層:函數
        class TestPositives:      # 第三層:測試分類
            def test_small_primes(self):  # 具體測試案例
                pass
            
            def test_large_primes(self):
                pass
        
        class TestBoundary:
            def test_handle_zero(self):
                pass
            
            def test_handle_negatives(self):
                pass

這種結構讓測試報告更易讀:

test_math_utils.py::TestMathUtils::TestIsPrime::TestPositives::test_small_primes ✓
test_math_utils.py::TestMathUtils::TestIsPrime::TestPositives::test_large_primes ✓
test_math_utils.py::TestMathUtils::TestIsPrime::TestBoundary::test_handle_zero ✓
test_math_utils.py::TestMathUtils::TestIsPrime::TestBoundary::test_handle_negatives ✓

實戰演練:重構散亂的測試 🔧

讓我們把昨天的測試重新組織。建立 tests/day04/test_math_utils.py

步驟 1:建立基本結構

from src.math.math_utils import is_prime, count_primes_in_range

class TestMathUtils:
    class TestIsPrime:
        """is_prime 質數判斷相關的所有測試"""
        pass
    
    class TestCountPrimesInRange:
        """count_primes_in_range 範圍質數計算相關的所有測試"""
        pass

步驟 2:組織 isPrime 測試

使用巢狀類別結構來清晰組織不同類型的測試案例。

測試分組策略 📊

按功能分組

最常見的分組方式是按函數分組:

class TestMathUtils:
    class TestIsPrime:
        pass
    
    class TestFibonacci:
        pass
    
    class TestFactorial:
        pass

按測試類型分組

在每個功能內,再按測試類型細分:

class TestIsPrime:
    class TestPositiveCases:
        """正向測試案例"""
        pass
    
    class TestBoundaryCases:
        """邊界測試案例"""
        pass
    
    class TestErrorHandling:
        """錯誤處理測試案例(Day 8 會詳細學習)"""
        pass

測試命名最佳實踐 🎯

命名原則

  1. 描述行為,不是實作
  2. 使用業務語言,不是技術術語
  3. 簡潔明確,避免冗長

好的命名範例

class TestIsPrime:
    # ✅ 好的命名:描述期望的行為
    def test_identify_prime_2(self):
        pass
    
    def test_identify_composite_4(self):
        pass
    
    def test_handle_negative_numbers(self):
        pass
    
    # ❌ 不好的命名:過於技術性或冗長
    def test_when_input_2_should_return_true(self):
        pass
    
    def test_is_prime_function_with_negative_input_should_return_false(self):
        pass

推薦的命名模式:test_[動詞][對象][條件]

# 模式:test_[動詞]_[對象]_[條件]
def test_identify_prime_2(self):           # 識別 + 質數 + 2
def test_reject_negative_numbers(self):    # 拒絕 + 負數
def test_calculate_primes_in_range(self):  # 計算 + 範圍內質數
def test_handle_empty_range(self):         # 處理 + 空範圍

定期執行測試以確保結構改善不會破壞既有功能。

實戰範例:完整測試結構 💻

完整實作 tests/day04/test_math_utils.py

# 建立 tests/day04/test_math_utils.py
from src.math.math_utils import is_prime, count_primes_in_range

class TestMathUtils:
    class TestIsPrime:
        class TestPrimeDetection:
            def test_identify_small_primes(self):
                assert is_prime(2) is True
                assert is_prime(3) is True
                assert is_prime(5) is True
                assert is_prime(7) is True

            def test_identify_large_primes(self):
                assert is_prime(11) is True
                assert is_prime(13) is True
                assert is_prime(17) is True
                assert is_prime(19) is True

        class TestCompositeDetection:
            def test_identify_small_composites(self):
                assert is_prime(4) is False
                assert is_prime(6) is False
                assert is_prime(8) is False
                assert is_prime(9) is False

            def test_identify_large_composites(self):
                assert is_prime(10) is False
                assert is_prime(12) is False
                assert is_prime(14) is False
                assert is_prime(15) is False

        class TestBoundaryConditions:
            def test_handle_numbers_less_than_2(self):
                assert is_prime(0) is False
                assert is_prime(1) is False
                assert is_prime(-1) is False
                assert is_prime(-10) is False

    class TestCountPrimesInRange:
        class TestValidRanges:
            def test_count_primes_in_small_range(self):
                assert count_primes_in_range(1, 10) == 4  # 2, 3, 5, 7
            
            def test_count_primes_in_medium_range(self):
                assert count_primes_in_range(10, 20) == 4  # 11, 13, 17, 19

        class TestEdgeCases:
            def test_handle_single_number_range(self):
                assert count_primes_in_range(2, 2) == 1
                assert count_primes_in_range(4, 4) == 0
            
            def test_handle_reversed_range(self):
                assert count_primes_in_range(10, 1) == 0

執行測試查看結構

pytest tests/day04/test_math_utils.py -v

輸出會顯示清晰的測試階層結構,讓你一眼就能看出測試的組織方式。

今天學到什麼? 🎉

今天我們從散亂的測試進化到有組織的測試結構:

技術收穫

  • 掌握測試套件概念:用類別創建階層結構
  • 學會測試分組策略:按功能、類型、場景分組
  • 掌握命名最佳實踐:清晰、簡潔、具描述性
  • 理解檔案組織原則:結構一致性

組織能力

  • 從混亂到有序:重新組織成清晰結構
  • 邏輯分類:相關測試聚在一起
  • 結構思考:先規劃再實作

實用技巧

  • 階層設計:模組 > 函數 > 測試類型 > 具體案例
  • 命名規範:test_動詞_對象_條件的命名模式
  • 可讀性提升:好的結構就是文檔

總結

測試結構與組織不只是美觀,更是實用。當專案變大、測試變多時,良好的組織結構能:

  • 提高開發效率:快速找到相關測試
  • 降低維護成本:新測試有明確歸屬
  • 提升代碼品質:結構化思考促進更好的設計
  • 改善團隊協作:統一的組織方式便於團隊理解

記住:好的測試結構是可維護測試代碼的基礎。

明天我們將學習「測試生命週期」,了解如何在測試執行前後進行必要的設置和清理工作,讓我們的測試更加穩定可靠。 💪


上一篇
Day 03 - TDD 紅綠重構循環
下一篇
Day 05 - 測試生命週期 🔄
系列文
Python pytest TDD 實戰:從零開始的測試驅動開發8
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言