單元測試(Unit Test)(020):從 assert 到 unittest

在軟體開發的世界裡,「沒有測試的程式,就像沒有安全帶的車」
即使是微小的錯誤,也可能引發巨大的損失:從火箭爆炸、航天任務失敗,到日常應用的嚴重故障。

在 Python 中,單元測試 (Unit Test) 是最常見也最重要的測試方法。它專注於驗證程式中最小的功能單元,例如函數或方法,確保它們在各種情境下都能正確運作。

為什麼測試如此重要?

軟體錯誤可能導致災難性後果:

  • 1996年:阿麗安5號火箭爆炸,損失5億美元
  • 1999年:火星氣候軌道器失敗,損失1.25億美元
  • 2015年:豐田汽車因軟體錯誤召回數百萬輛車
  • 2016年:航空管制系統故障導致全球航班延誤

「軟體測試的目標不僅是找出錯誤,更是要盡早發現它們。」

適當的測試可以防止災難性的後果,保護使用者安全並節省成本。

rocket explosion due to software error, dramatic failure

測試的類型

手動測試

由人員進行測試,模擬真實使用者的行為。

  • 執行程式並觀察結果
  • 透過使用者介面互動
  • 檢查輸出是否符合預期

優點:直觀、無需編寫額外代碼

缺點:耗時、容易出現人為錯誤

自動化測試

使用程式碼自動執行測試過程。

  • 單元測試:測試最小功能單元
  • 整合測試:測試多個單元的互動
  • 系統測試:測試整個應用程式

優點:快速、可重複、更少人為錯誤

缺點:需要額外編寫測試代碼

在本課程中,我們將重點關注自動化測試,特別是單元測試。

什麼是單元測試?

單元測試是測試程式中最小功能單元的過程,通常是單個函數或方法。它就像檢查門把手、鉸鏈和鎖以確保整扇門正常運作。

單元測試的好處:

  • 及早發現問題,降低修復成本
  • 提高程式碼品質和可維護性
  • 幫助理解程式碼行為
  • 作為程式碼的活文檔
  • 增強重構的信心

每個單元測試應該針對一個特定的功能,並驗證它在各種輸入下的行為是否正確。

puzzle pieces representing software units being tested individually

assert 陳述式:最簡單的測試方法

Python提供了一個簡單的測試機制:assert陳述式。它用於驗證條件是否為真。

# 基本語法
assert 條件, '條件不滿足時的錯誤訊息'

# 例子
def 乘以十(數字):
    return 數字 * 100  # 注意這裡有錯誤

結果 = 乘以十(20)
assert 結果 == 200, '預期乘以十(20)會返回200,但得到' + str(結果)

# 運行結果
AssertionError: 預期乘以十(20)會返回200,但得到2000

當條件評估為False時,將引發AssertionError並顯示錯誤訊息。

assert陳述式是快速檢查程式狀態的有力工具,但對於複雜測試場景有局限性。

unittest框架

Python的標準庫提供了unittest模組,一個功能豐富的測試框架。它解決了單獨使用assert的幾個問題:

測試收集和執行

自動收集和執行測試,提供完整的測試報告

測試隔離

即使某個測試失敗,其他測試仍會繼續執行

測試組織

將相關測試分組到測試類中,便於管理和維護

豐富的斷言方法

提供比簡單assert更強大的測試方法

測試夾具

為測試提供設置和清理機制

執行後,框架會報告測試通過或失敗,幫助我們快速定位錯誤。

unittest 常用斷言方法

  • 相等性
    • assertEqual(a, b):檢查是否相等
    • assertNotEqual(a, b):檢查是否不相等
  • 布林判斷
    • assertTrue(x)assertFalse(x)
  • 成員關係
    • assertIn(x, y)assertNotIn(x, y)
  • 數值比較
    • assertLess(a, b)assertGreaterEqual(a, b)
    • assertAlmostEqual(a, b, places=2):浮點數比較
  • 異常與警告
    • assertRaises(Exception, func, args...)
    • assertWarns(Warning, func, args...)

參數化測試

參數化測試讓我們可以用一個測試方法測試多個輸入,減少代碼重複。

def test_乘以十(self):
    測試數據 = [0, 1000000, -10]
    for 數字 in 測試數據:
        with self.subTest(數字):
            預期結果 = 數字 * 10
            訊息 = f'預期乘以十({數字})會返回{預期結果}'
            self.assertEqual(乘以十(數字), 預期結果, 訊息)

subTest上下文管理器將每次迭代視為單獨的測試,如果一個失敗,其他測試仍會繼續執行。

這種方法的優點:

  • 程式碼更簡潔,易於維護
  • 輕鬆擴展測試覆蓋範圍
  • 測試報告更清晰

參數化測試使我們能夠輕鬆地測試多種情況,而不必為每種情況編寫單獨的測試方法。

提示:為subTest提供參數(如示例中的數字)可以使測試報告更清晰。

跳過測試

有時我們需要在特定條件下跳過某些測試,unittest提供了兩種方式:

使用裝飾器

import sys

@unittest.skipUnless(sys.platform.startswith("linux"), 
                   "此測試僅在Linux上運行")
def test_linux功能(self):
    print("此測試僅在Linux上運行")

@unittest.skipIf(not sys.platform.startswith("linux"), 
                "此測試僅在Linux上運行")
def test_其他linux功能(self):
    print("此測試僅在Linux上運行")

使用skipTest方法

def test_linux功能(self):
    if not sys.platform.startswith("linux"):
        self.skipTest("測試僅在Linux上運行")

常用的測試跳過裝飾器:

  • @unittest.skip(reason):無條件跳過
  • @unittest.skipIf(condition, reason):條件為真時跳過
  • @unittest.skipUnless(condition, reason):條件為假時跳過

跳過的測試在結果中會被標記為skipped,而不是失敗或成功。

filtration system showing tests being skipped based on conditions

預期失敗

有時我們知道某個測試會失敗(例如已知的錯誤或故意失敗的設計),但我們不希望這種預期的失敗影響測試結果。

@unittest.expectedFailure
def test_已知問題(self):
    self.assertEqual(有錯誤的函數(), 預期結果)

使用@unittest.expectedFailure裝飾器:

  • 如果測試失敗:標記為「預期失敗」(expected failure),視為通過
  • 如果測試通過:標記為「意外成功」(unexpected success),視為失敗

這對於處理以下情況很有用:

  • 記錄已知但尚未修復的錯誤
  • 跟踪進行中的開發工作
  • 測試負面情況(即測試應該失敗的情況)

結語

測試不是額外的工作,而是讓你更快、更有信心開發的工具。

assertunittest,再到進階的 pytest 與 CI/CD,自動化測試將成為每位 Python 開發者不可或缺的技能。

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *

返回頂端