返回文章列表

Python 測試實務與檔案撰寫

本文介紹 Python 測試基礎與實務應用,涵蓋 unittest、doctest 模組的使用,並探討 mocking 技術。同時,文章也深入講解檔案撰寫的最佳實務,包含 Sphinx 的使用、reStructuredText 語法,以及 logging 模組的有效運用。

軟體測試 Python

Python 測試生態系豐富多元,從內建的 unittestdoctest 到第三方框架 pytest,都能協助開發者確保程式碼品質。unittest 提供類別似 JUnit 的 API,方便撰寫單元測試,而 doctest 則能直接在檔案字串中嵌入測試案例。此外,unittest.mock 模組的 mocking 技術,讓測試更具彈性。除了測試,良好的檔案也是專案成功的關鍵。Sphinx 作為 Python 社群常用的檔案生成工具,搭配 reStructuredText 語法,能產出易讀易懂的檔案。而 logging 模組的正確使用,則能有效記錄程式執行資訊,方便除錯與分析。

測試基礎與實務應用

在軟體開發過程中,測試是確保程式碼品質的關鍵步驟。本文將介紹 Python 中的測試基礎,包括 unittestdoctest 模組的使用方法,並透過實際範例展示如何撰寫有效的測試。

使用 unittest 進行單元測試

unittest 是 Python 標準函式庫中的測試模組,其 API 與 JUnit(Java)、nUnit(.NET)和 CppUnit(C/C++)等測試工具類別似。透過繼承 unittest.TestCase,可以建立測試案例。

範例:使用 unittest 測試函式

# test_example.py
import unittest

def fun(x):
    return x + 1

class MyTest(unittest.TestCase):
    def test_that_fun_adds_one(self):
        self.assertEqual(fun(3), 4)

class MySecondTest(unittest.TestCase):
    def test_that_fun_fails_when_not_adding_number(self):
        self.assertRaises(TypeError, fun, "multiply six by nine")

在這個範例中,我們定義了一個簡單的函式 fun(x),並使用 unittest.TestCase 建立了兩個測試案例。測試方法必須以 test 開頭,才能被執行。

執行測試

$ python -m unittest test_example.MyTest
.
---
-
---
-
---
-
---
-
---
-
---
-
---
-
---
-
---
-
---
-
---
-
---
-
---
-
---
-
---
-
---
-
---
---
Ran 1 test in 0.000s
OK

可以使用 python -m unittest 命令執行測試,並指定測試模組或測試類別。

使用 unittest.mock 進行 Mocking

從 Python 3.3 開始,unittest.mock 成為標準函式庫的一部分。這個模組允許我們替換系統中的部分元件,並對其行為進行斷言。

範例:使用 MagicMock 替換方法

from unittest.mock import MagicMock

instance = ProductionClass()
instance.method = MagicMock(return_value=3)
instance.method(3, 4, 5, key='value')
instance.method.assert_called_with(3, 4, 5, key='value')

在這個範例中,我們使用 MagicMock 替換了 ProductionClassmethod 方法,並對其呼叫行為進行斷言。

使用 doctest 進行檔案測試

doctest 模組會在檔案字串中搜尋類別似互動式 Python 會話的文字,並執行這些會話以驗證其行為是否正確。

範例:使用 doctest 測試函式

def square(x):
    """Squares x.
    >>> square(2)
    4
    >>> square(-2)
    4
    """
    return x * x

if __name__ == '__main__':
    import doctest
    doctest.testmod()

在這個範例中,我們定義了一個 square(x) 函式,並在其檔案字串中包含了測試案例。當執行這個模組時,doctest 會自動執行這些測試。

實務應用:Tablib 的測試範例

Tablib 使用 unittest 模組進行測試。以下是其測試程式碼的摘錄:

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""Tests for Tablib."""
import unittest
import tablib

class TablibTestCase(unittest.TestCase):
    def setUp(self):
        """Create simple data set with headers."""
        global data, book
        data = tablib.Dataset()
        book = tablib.Databook()

    def test_empty_append(self):
        """Verify append() correctly adds tuple with no headers."""
        new_row = (1, 2, 3)
        data.append(new_row)
        self.assertTrue(data.width == len(new_row))
        self.assertTrue(data[0] == new_row)

在這個範例中,我們可以看到 Tablib 如何使用 unittest 建立測試案例,並對其資料集進行測試。

程式碼解密:

  1. setUp 方法:在每個測試方法執行前被呼叫,用於建立測試所需的資料集。
  2. test_empty_append 方法:測試 append() 方法是否正確地增加了沒有標題的元組。
  3. assertTrue 方法:用於斷言條件是否為真,若條件不成立,則測試失敗。

單元測試與測試框架

單元測試是軟體開發中不可或缺的一環,旨在驗證程式碼的正確性和穩定性。Python 提供了多種測試框架,包括內建的 unittest 模組,以及第三方框架如 pytestnose

使用 Unittest 進行測試

Python 的 unittest 模組提供了一套完整的測試框架。要使用 unittest,需要建立一個繼承自 unittest.TestCase 的類別,並在其中定義以 test 開頭的方法。

import unittest

def func(x):
    return x + 1

class TestFunc(unittest.TestCase):
    def test_answer(self):
        self.assertEqual(func(3), 4)

if __name__ == '__main__':
    unittest.main()

內容解密:

  1. unittest.TestCase 是所有測試案例的基礎類別,提供了豐富的斷言方法,如 assertEqualassertTrue 等。
  2. 測試方法必須以 test 開頭,否則不會被執行。
  3. setUp 方法在每個測試方法執行前被呼叫,用於準備測試環境。
  4. tearDown 方法在每個測試方法執行後被呼叫,用於清理測試環境。
  5. addCleanup 方法可用於註冊清理函式,確保資源被正確釋放。

使用 Pytest 進行測試

pytest 是一個功能強大且易用的測試框架,相比 unittest,它提供了更簡潔的語法和更強大的功能。

# content of test_sample.py
def func(x):
    return x + 1

def test_answer():
    assert func(3) == 4

執行 pytest 命令即可執行測試:

$ pytest
=========================== test session starts ============================
platform darwin -- Python 3.9.0, pytest-6.2.4, py-1.10.0, pluggy-0.13.1
rootdir: /tmp/test
collected 1 item

test_sample.py .                                                     [100%]

============================ 1 passed in 0.01s =============================

內容解密:

  1. pytest 不需要顯式定義測試類別,直接定義以 test 開頭的函式即可。
  2. 使用 assert 陳述式進行斷言,語法簡潔直觀。
  3. pytest 自動發現測試,無需手動註冊。

其他測試工具

除了 unittestpytest,還有其他流行的測試工具,如 nose。這些工具各有其特點和優勢,可以根據專案需求選擇合適的測試框架。

檔案與測試最佳實踐

在軟體開發過程中,測試和檔案撰寫是至關重要的環節。本文將介紹Python社群中常用的測試工具和檔案撰寫最佳實踐。

自動化測試工具

Python提供了多種自動化測試工具,以簡化測試流程並提高程式碼品質。

Nose

Nose是一個自動化測試框架,提供測試發現功能,可自動尋找並執行測試案例。此外,Nose還提供多種外掛,如xUnit相容的測試輸出、覆寫率報告和測試選擇。

Tox

Tox是一個自動化測試環境管理工具,支援多版本Python環境的測試。

$ pip install tox

Tox透過簡單的INI樣式設定檔,允許使用者組態複雜的多引數測試矩陣。

相容舊版Python的工具

對於無法控制Python版本的使用者,可以使用以下工具:

  • unittest2:Python 2.7的unittest模組的回溯移植,提供了改進的API和更好的斷言功能。

$ pip install unittest2

    ```python
import unittest2 as unittest
class MyTest(unittest.TestCase):
    ...
  • Mock:unittest.mock的獨立函式庫,提供了模擬物件的功能。

$ pip install mock


### 行為驅動開發(BDD)

Lettuce和Behave是兩個用於Python的BDD工具。BDD是一種源自TDD的開發流程,強調以「行為」取代「測試」。

#### Gherkin語法

Gherkin是一種人類可讀且機器可處理的語法,用於描述行為。

```gherkin
# Gherkin 語法範例
Feature: 範例功能
    Scenario: 範例情境
        Given 前置條件
        When 動作發生
        Then 結果出現

檔案撰寫最佳實踐

專案檔案

專案檔案應包括README、INSTALL、LICENSE、TODO和CHANGELOG等檔案。

  • README:專案的主要入口點,應包含專案目的、原始碼網址和基本貢獻資訊。
  • LICENSE:指定軟體的授權許可。
  • TODO:列出計畫中的開發工作。
  • CHANGELOG:記錄最新版本的變更。

專案發布

專案檔案可能包括簡介、教學、API參考和開發者檔案等元件。

  • 簡介:提供產品的簡要概述。
  • 教學:逐步引導讀者建立工作原型。
  • API參考:列出所有公開介面、引數和傳回值。
  • 開發者檔案:提供貢獻者所需的檔案,如程式碼慣例和設計策略。

Sphinx檔案工具

Sphinx是Python社群中最流行的檔案工具,支援將reStructuredText標記語言轉換為多種輸出格式,包括HTML、LaTeX和純文字。

# 安裝Sphinx
$ pip install sphinx

Read the Docs

Read the Docs是一個免費的Sphinx檔案託管服務,支援與原始碼儲存函式庫的提交鉤子,以實作自動重建檔案。

專業檔案寫作與紀錄管理

使用 Sphinx 進行檔案編寫

Sphinx 不僅以其 API 生成能力聞名,同時也非常適合用於一般專案的檔案編寫。著名的線上資源《Python Hitchhiker’s Guide》便是使用 Sphinx 建立,並託管於 Read the Docs。

reStructured Text 語法

Sphinx 使用 reStructured Text(RST)作為其主要的語法格式,幾乎所有的 Python 檔案都是使用 RST 編寫。如果在 setuptools.setup()long_description 引數中使用 RST 編寫內容,那麼它將在 PyPI 上被渲染為 HTML 格式,而其他格式則僅會以純文字形式呈現。RST 可以視為一種具有內建多種擴充功能的 Markdown 語法。學習 RST 語法可參考以下資源:

  • The reStructuredText Primer
  • reStructuredText Quick Reference

或者,可以透過為您喜愛的套件貢獻檔案,在閱讀中學習 RST 語法。

檔案字串(Docstring)與區塊註解的區別

檔案字串和區塊註解並非可以互換使用。兩者都可以被用於函式或類別。以下是一個同時使用兩者的範例:

# 這個函式因為某些原因減慢了程式執行速度。
def square_and_rooter(x):
    """傳回 self 乘以 self 的平方根。"""
    ...

前面的註解區塊是給程式開發者的註解,而檔案字串則描述了函式或類別的操作,並將在互動式 Python 環境中透過 help(square_and_rooter) 顯示。放置在模組開頭或 __init__.py 檔案頂部的檔案字串,也會在 help() 中顯示。Sphinx 的 autodoc 功能可以自動根據適當格式的檔案字串生成檔案。有關如何進行此操作的說明,以及如何為 autodoc 格式化檔案字串,請參閱 Sphinx 教學。

程式碼範例與詳細解說

def example_function():
    """範例函式的說明。"""
    pass

內容解密:

  1. def example_function(): 定義了一個名為 example_function 的函式。
  2. """範例函式的說明。""" 是該函式的檔案字串,用於描述函式的功能。
  3. pass 是 Python 中的一個空操作陳述式,當該函式被呼叫時,不會執行任何操作。

紀錄(Logging)功能

Python 的標準函式庫自版本 2.3 起就包含了 logging 模組。它在 PEP 282 中有簡潔的描述,但檔案本身被認為是出了名的難讀,尤其是對於初學者,除了基本的 logging 教學以外。

紀錄的目的

Logging 主要服務於兩個目的:

  1. 診斷紀錄(Diagnostic Logging):記錄與應用程式操作相關的事件,以便在需要時查詢相關的上下文資訊。
  2. 稽核紀錄(Audit Logging):記錄用於商業分析的事件,例如使用者的交易記錄,可以與其他使用者資訊結合,生成報告或最佳化商業目標。

為何選擇 Logging 而非 Print

只有在需要為命令列應用程式顯示幫助陳述式時,print 才會比 logging 更合適。其他情況下,logging 優於 print 的原因包括:

  • 紀錄事件所建立的紀錄包含易於取得的診斷資訊,如檔名、完整路徑、函式名稱和行號。
  • 被包含模組中的紀錄事件可以自動被根紀錄器存取,除非被過濾掉。
  • 可以透過呼叫 logging.Logger.setLevel() 方法或將 logging.Logger.disabled 屬性設為 True 來選擇性地靜音或停用紀錄。

在函式庫中使用 Logging

在設定函式庫的 logging 時,有幾點需要注意。有關更多資訊,請參閱 logging 教學。另一個好的資源是下章節中提到的函式庫範例。由於使用者而非函式庫決定當紀錄事件發生時應該怎麼做,因此有一項重要的建議需要重複: 強烈建議不要在您的函式庫的紀錄器中新增除了 NullHandler 之外的任何處理器。

最佳實務是在例項化紀錄器時,只使用 __name__ 全域變數:logging 模組使用點號表示法建立了一個紀錄器的層次結構,因此使用 __name__ 可以確保不會發生名稱衝突。

# 設定預設的 logging 處理器,以避免出現 "No handler found" 警告。
import logging
try: # Python 2.7+
    from logging import NullHandler
except ImportError:
    class NullHandler(logging.Handler):
        def emit(self, record):
            pass
logging.getLogger(__name__).addHandler(NullHandler())

內容解密:

  1. 程式碼首先匯入了 logging 模組。
  2. 試圖從 logging 中匯入 NullHandler,這是為了相容 Python 2.7 以上版本。
  3. 如果匯入失敗(即 Python 版本低於 2.7),則定義了一個自定義的 NullHandler 類別,該類別繼承自 logging.Handler,並在其 emit 方法中不執行任何操作。
  4. 最後,將 NullHandler 新增到由 __name__ 命名的紀錄器中,以避免在未組態處理器的情況下輸出 “No handler found” 警告。

在應用程式中使用 Logging

《The Twelve-Factor App》,一個對於應用程式開發最佳實務具有權威性的參考資料,其中有一節專門討論了關於 logging 的最佳實務。它強調應該將紀錄事件視為一個事件流,並將該事件流傳送到標準輸出,以便由應用程式環境進行處理。

設定紀錄器的方法至少有三種:

  1. 使用 INI 格式的檔案:可以在執行時更新組態,但相比直接在程式碼中組態紀錄器,靈活性較低。
  2. 使用字典或 JSON 格式的檔案:除了可以在執行時更新組態外,還可以使用 Python 標準函式庫中的 json 模組從檔案載入組態。
  3. 直接在程式碼中組態:提供了對組態的完全控制,但任何修改都需要更改原始碼。

範例:透過 INI 檔案進行組態

更多關於 INI 檔案格式的細節,請參閱 logging 教學中的 logging 組態章節。一個最小化的組態檔案範例如下:

[loggers]
keys=root
[handlers]
keys=stream_handler
[formatters]
keys=formatter
[logger_root]
level=DEBUG
handlers=stream_handler
[handler_stream_handler]
class=StreamHandler
level=DEBUG
formatter=formatter
args=(sys.stderr,)
[formatter_formatter]
format=%(asctime)s - %(name)s - %(levelname)s - %(message)s

內容解密:

  1. [loggers][handlers][formatters] 分別定義了紀錄器、處理器和格式化器的鍵值。
  2. [logger_root] 設定了根紀錄器的級別和處理器。
  3. [handler_stream_handler] 定義了一個串流處理器,並指定了其級別和格式化器。
  4. [formatter_formatter] 定義了紀錄訊息的格式,包括時間戳、紀錄器名稱、日誌級別和訊息內容。