doctest --- 測試互動式 Python 示例

原始碼: Lib/doctest.py


doctest 模組會搜尋看起來像互動式 Python 會話的文字片段,然後執行這些會話,以驗證它們是否完全按照所示的方式工作。有幾種常見的方式來使用 doctest:

  • 透過驗證所有互動式示例仍然按文件所述工作,來檢查模組的文件字串是否最新。

  • 透過驗證測試檔案或測試物件中的互動式示例是否按預期工作,來執行迴歸測試。

  • 為一個包編寫教程文件,並大量使用輸入輸出示例進行說明。根據重點是示例還是說明性文字,這具有“文學式測試”或“可執行文件”的風格。

這是一個完整但很小的示例模組:

"""
This is the "example" module.

The example module supplies one function, factorial().  For example,

>>> factorial(5)
120
"""

def factorial(n):
    """Return the factorial of n, an exact integer >= 0.

    >>> [factorial(n) for n in range(6)]
    [1, 1, 2, 6, 24, 120]
    >>> factorial(30)
    265252859812191058636308480000000
    >>> factorial(-1)
    Traceback (most recent call last):
        ...
    ValueError: n must be >= 0

    Factorials of floats are OK, but the float must be an exact integer:
    >>> factorial(30.1)
    Traceback (most recent call last):
        ...
    ValueError: n must be exact integer
    >>> factorial(30.0)
    265252859812191058636308480000000

    It must also not be ridiculously large:
    >>> factorial(1e100)
    Traceback (most recent call last):
        ...
    OverflowError: n too large
    """

    import math
    if not n >= 0:
        raise ValueError("n must be >= 0")
    if math.floor(n) != n:
        raise ValueError("n must be exact integer")
    if n+1 == n:  # catch a value like 1e300
        raise OverflowError("n too large")
    result = 1
    factor = 2
    while factor <= n:
        result *= factor
        factor += 1
    return result


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

如果你直接從命令列執行 example.pydoctest 就會發揮它的魔力:

$ python example.py
$

沒有任何輸出!這很正常,意味著所有示例都通過了。向指令碼傳遞 -vdoctest 會列印一份它嘗試執行的詳細日誌,並在末尾列印一個摘要:

$ python example.py -v
Trying:
    factorial(5)
Expecting:
    120
ok
Trying:
    [factorial(n) for n in range(6)]
Expecting:
    [1, 1, 2, 6, 24, 120]
ok

等等,最終以以下內容結束:

Trying:
    factorial(1e100)
Expecting:
    Traceback (most recent call last):
        ...
    OverflowError: n too large
ok
2 items passed all tests:
   1 test in __main__
   6 tests in __main__.factorial
7 tests in 2 items.
7 passed.
Test passed.
$

這就是開始高效使用 doctest 所需瞭解的全部內容!開始使用吧。以下各節提供了完整的詳細資訊。請注意,標準 Python 測試套件和庫中有許多 doctest 的例子。特別有用的例子可以在標準測試檔案 Lib/test/test_doctest/test_doctest.py 中找到。

在 3.13 版本加入: 輸出預設是彩色的,並且可以透過環境變數進行控制

簡單用法:檢查文件字串中的示例

開始使用 doctest 最簡單的方法(但不一定是你將繼續使用的方式)是在每個模組 M 的末尾加上:

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

然後,doctest 會檢查模組 M 中的文件字串。

將模組作為指令碼執行,會導致文件字串中的示例被執行和驗證:

python M.py

除非有示例失敗,否則這不會顯示任何內容。如果示例失敗,失敗的示例及其原因將被列印到標準輸出,輸出的最後一行是 ***Test Failed*** N failures.,其中 N 是失敗示例的數量。

改用 -v 開關執行它:

python M.py -v

然後,所有嘗試的示例的詳細報告將被列印到標準輸出,並在末尾附有各種摘要。

你可以透過向 testmod() 傳遞 verbose=True 來強制進入詳細模式,或者透過傳遞 verbose=False 來禁止它。在這兩種情況下,sys.argv 都不會被 testmod() 檢查(所以傳遞 -v 或不傳遞都沒有效果)。

還有一個用於執行 testmod() 的命令列快捷方式,請參見 命令列用法 部分。

關於 testmod() 的更多資訊,請參見 基本 API 部分。

簡單用法:檢查文字檔案中的示例

doctest 的另一個簡單應用是測試文字檔案中的互動式示例。這可以透過 testfile() 函式完成:

import doctest
doctest.testfile("example.txt")

這段簡短的指令碼會執行並驗證檔案 example.txt 中包含的任何互動式 Python 示例。檔案內容被視為一個巨大的文件字串;該檔案不需要包含 Python 程式!例如,也許 example.txt 包含以下內容:

The ``example`` module
======================

Using ``factorial``
-------------------

This is an example text file in reStructuredText format.  First import
``factorial`` from the ``example`` module:

    >>> from example import factorial

Now use it:

    >>> factorial(6)
    120

執行 doctest.testfile("example.txt") 會發現此文件中的錯誤:

File "./example.txt", line 14, in example.txt
Failed example:
    factorial(6)
Expected:
    120
Got:
    720

testmod() 一樣,testfile() 除非有示例失敗,否則不會顯示任何內容。如果示例失敗,失敗的示例及其原因將使用與 testmod() 相同的格式列印到標準輸出。

預設情況下,testfile() 會在呼叫模組的目錄中查詢檔案。有關可用於讓它在其他位置查詢檔案的可選引數的描述,請參見 基本 API 部分。

testmod() 一樣,testfile() 的詳細程度可以透過 -v 命令列開關或可選的關鍵字引數 *verbose* 來設定。

還有一個用於執行 testfile() 的命令列快捷方式,請參見 命令列用法 部分。

關於 testfile() 的更多資訊,請參見 基本 API 部分。

命令列用法

doctest 模組可以從命令列作為指令碼呼叫:

python -m doctest [-v] [-o OPTION] [-f] file [file ...]
-v, --verbose

所有嘗試過的示例的詳細報告會列印到標準輸出,並在末尾附上各種摘要:

python -m doctest -v example.py

這將把 example.py 作為一個獨立的模組匯入,並對其執行 testmod()。請注意,如果檔案是包的一部分並且從該包匯入其他子模組,這可能無法正常工作。

如果檔名不以 .py 結尾,doctest 會推斷它必須使用 testfile() 執行:

python -m doctest -v example.txt
-o, --option <option>

選項標誌控制 doctest 行為的各個方面,請參見 選項標誌 部分。

在 3.4 版本加入。

-f, --fail-fast

這是 -o FAIL_FAST 的簡寫。

在 3.4 版本加入。

工作原理

本節詳細闡述了 doctest 的工作原理:它檢查哪些文件字串,如何找到互動式示例,使用什麼執行上下文,如何處理異常,以及如何使用選項標誌來控制其行為。這些是編寫 doctest 示例需要知道的資訊;關於如何在這些示例上實際執行 doctest 的資訊,請參見以下各節。

檢查哪些文件字串?

模組文件字串以及所有函式、類和方法的文件字串都會被搜尋。匯入到模組中的物件不會被搜尋。

此外,在某些情況下,你希望測試成為模組的一部分,但不屬於幫助文字,這就要求測試不包含在文件字串中。Doctest 會查詢一個名為 __test__ 的模組級變數,並用它來定位其他測試。如果 M.__test__ 存在,它必須是一個字典,每個條目將一個(字串)名稱對映到一個函式物件、類物件或字串。從 M.__test__ 中找到的函式和類物件的文件字串會被搜尋,而字串則被視為文件字串。在輸出中,M.__test__ 中的鍵 K 將以 M.__test__.K 的名稱出現。

例如,將以下程式碼塊放在 example.py 的頂部:

__test__ = {
    'numbers': """
>>> factorial(6)
720

>>> [factorial(n) for n in range(6)]
[1, 1, 2, 6, 24, 120]
"""
}

example.__test__["numbers"] 的值將被視為一個文件字串,其中所有的測試都將被執行。需要注意的是,該值可以對映到一個函式、類物件或模組;如果是這樣,doctest 會遞迴地搜尋它們的文件字串,然後掃描這些文件字串以查詢測試。

任何找到的類都會被類似地遞迴搜尋,以測試其包含的方法和巢狀類中的文件字串。

備註

doctest 只能自動發現在模組級別或其他類內部定義的類和函式。

由於巢狀的類和函式僅在外部函式被呼叫時才存在,因此無法被發現。請在外部定義它們以使其可見。

如何識別文件字串示例?

在大多數情況下,複製貼上互動式控制檯會話的內容效果很好,但 doctest 並不試圖精確模擬任何特定的 Python shell。

>>> # comments are ignored
>>> x = 12
>>> x
12
>>> if x == 13:
...     print("yes")
... else:
...     print("no")
...     print("NO")
...     print("NO!!!")
...
no
NO
NO!!!
>>>

任何預期的輸出都必須緊跟在包含程式碼的最後一行 '>>> ''... ' 之後,並且預期的輸出(如果有的話)會延伸到下一個 '>>> ' 或全為空白字元的行。

詳細說明:

  • 預期輸出不能包含一個全為空白字元的行,因為這樣的行被視為預期輸出結束的訊號。如果預期輸出確實包含空行,請在你的 doctest 示例中每個預期空行的地方放置 <BLANKLINE>

  • 所有硬製表符字元都會被擴充套件為空格,使用 8 列製表位。被測試程式碼生成的輸出中的製表符不會被修改。因為示例輸出中的任何硬製表符*都*會被擴充套件,這意味著如果程式碼輸出包含硬製表符,doctest 能夠透過的唯一方法是 NORMALIZE_WHITESPACE 選項或 指令 生效。或者,可以重寫測試以捕獲輸出,並將其與預期值進行比較作為測試的一部分。這種對原始碼中製表符的處理是透過反覆試驗得出的,並被證明是處理它們時最不容易出錯的方式。可以透過編寫自定義的 DocTestParser 類來使用不同的演算法處理製表符。

  • 標準輸出 (stdout) 會被捕獲,但標準錯誤 (stderr) 的輸出不會(異常回溯透過另一種方式捕獲)。

  • 如果你在互動式會話中透過反斜槓續行,或因任何其他原因使用反斜槓,你應該使用原始文件字串,它會完全按照你輸入的方式保留你的反斜槓:

    >>> def f(x):
    ...     r'''Backslashes in a raw docstring: m\n'''
    ...
    >>> print(f.__doc__)
    Backslashes in a raw docstring: m\n
    

    否則,反斜槓將被解釋為字串的一部分。例如,上面的 \n 將被解釋為一個換行符。或者,你可以在 doctest 版本中將每個反斜槓加倍(並且不使用原始字串):

    >>> def f(x):
    ...     '''Backslashes in a raw docstring: m\\n'''
    ...
    >>> print(f.__doc__)
    Backslashes in a raw docstring: m\n
    
  • 起始列無關緊要:

    >>> assert "Easy!"
          >>> import math
              >>> math.floor(1.9)
              1
    

    並且從預期輸出中剝離的開頭空白字元數量與開始示例的初始 '>>> ' 行中的數量相同。

執行上下文是什麼?

預設情況下,每次 doctest 找到一個要測試的文件字串時,它會使用 M 的全域性變數的*淺複製*,這樣執行測試就不會改變模組的實際全域性變數,並且 M 中的一個測試不會留下意外讓另一個測試透過的“碎屑”。這意味著示例可以自由使用在 M 的頂層定義的任何名稱,以及在正在執行的文件字串中較早定義的名稱。示例看不到在其他文件字串中定義的名稱。

你可以透過向 testmod()testfile() 傳遞 globs=your_dict 來強制使用你自己的字典作為執行上下文。

異常怎麼辦?

沒問題,只要回溯資訊是示例產生的唯一輸出:只需將回溯資訊貼上進去即可。[1] 由於回溯資訊包含可能迅速變化的細節(例如,確切的檔案路徑和行號),doctest 在這種情況下會盡力靈活地接受輸入。

簡單示例:

>>> [1, 2, 3].remove(42)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: list.remove(x): x not in list

如果引發了 ValueError,並且詳細資訊為 list.remove(x): x not in list,那麼該 doctest 將會成功。

異常的預期輸出必須以回溯頭開始,可以是以下兩行中的任意一行,縮排與示例的第一行相同:

Traceback (most recent call last):
Traceback (innermost last):

回溯頭後面是可選的回溯堆疊,其內容被 doctest 忽略。回溯堆疊通常被省略,或者直接從互動式會話中複製。

回溯堆疊之後是最有趣的部分:包含異常型別和詳細資訊的行。這通常是回溯的最後一行,但如果異常的詳細資訊跨越多行,它也可以延伸到多行:

>>> raise ValueError('multi\n    line\ndetail')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: multi
    line
detail

最後三行(以 ValueError 開頭)會與異常的型別和詳細資訊進行比較,其餘的則被忽略。

最佳實踐是省略回溯堆疊,除非它為示例增加了重要的文件價值。所以最後一個示例可能這樣寫更好:

>>> raise ValueError('multi\n    line\ndetail')
Traceback (most recent call last):
    ...
ValueError: multi
    line
detail

請注意,回溯資訊被特殊處理。特別是在重寫的示例中,... 的使用與 doctest 的 ELLIPSIS 選項無關。該示例中的省略號可以省略,也可以是三個(或三百個)逗號或數字,或是一段縮排的 Monty Python 小品劇本。

一些你應該讀一遍但不需要記住的細節:

  • Doctest 無法猜測你的預期輸出是來自異常回溯還是普通列印。所以,例如,一個期望輸出為 ValueError: 42 is prime 的示例,無論 ValueError 是否真的被引發,還是示例僅僅列印了那段回溯文字,都會透過。在實踐中,普通輸出很少以回溯頭行開始,所以這不會造成實際問題。

  • 回溯堆疊的每一行(如果存在)必須比示例的第一行縮排更多,*或者*以非字母數字字元開頭。回溯頭之後的第一行,如果縮排相同且以字母數字開頭,則被視為異常詳細資訊的開始。當然,這對於真正的回溯是正確的。

  • 當指定了 IGNORE_EXCEPTION_DETAIL doctest 選項時,異常名稱中最左邊的冒號之後的所有內容以及任何模組資訊都會被忽略。

  • 對於某些 SyntaxError,互動式 shell 會省略回溯頭行。但 doctest 使用回溯頭行來區分異常和非異常。因此,在極少數情況下,當你需要測試一個省略了回溯頭行的 SyntaxError 時,你需要手動將回溯頭行新增到你的測試示例中。

  • 對於某些異常,Python 使用 ^ 標記和波浪線來顯示錯誤的位置:

    >>> 1 + None
      File "<stdin>", line 1
        1 + None
        ~~^~~~~~
    TypeError: unsupported operand type(s) for +: 'int' and 'NoneType'
    

    由於顯示錯誤位置的行出現在異常型別和詳細資訊之前,它們不會被 doctest 檢查。例如,下面的測試會透過,即使它把 ^ 標記放在了錯誤的位置:

    >>> 1 + None
      File "<stdin>", line 1
        1 + None
        ^~~~~~~~
    TypeError: unsupported operand type(s) for +: 'int' and 'NoneType'
    

選項標誌

許多選項標誌控制著 doctest 行為的各個方面。這些標誌的符號名稱作為模組常量提供,可以透過按位或運算組合在一起,並傳遞給各種函式。這些名稱也可以在doctest 指令中使用,並且可以透過 -o 選項傳遞給 doctest 命令列介面。

第一組選項定義了測試語義,控制 doctest 如何決定實際輸出是否與示例的預期輸出匹配的各個方面:

doctest.DONT_ACCEPT_TRUE_FOR_1

預設情況下,如果一個預期輸出塊只包含 1,那麼一個只包含 1 或只包含 True 的實際輸出塊被認為是匹配的,同樣適用於 0False。當指定了 DONT_ACCEPT_TRUE_FOR_1 時,這兩種替換都不被允許。預設行為是為了適應 Python 將許多函式的返回型別從整數改為布林值;期望“小整數”輸出的 doctest 在這些情況下仍然有效。這個選項可能會在未來移除,但不是在幾年內。

doctest.DONT_ACCEPT_BLANKLINE

預設情況下,如果預期輸出塊中有一行只包含字串 <BLANKLINE>,那麼該行將與實際輸出中的一個空行匹配。因為一個真正的空行會界定預期輸出,這是傳達期望一個空行的唯一方式。當指定了 DONT_ACCEPT_BLANKLINE 時,這種替換是不允許的。

doctest.NORMALIZE_WHITESPACE

當指定時,所有空白序列(空格和換行符)都被視為相等。預期輸出中的任何空白序列將與實際輸出中的任何空白序列匹配。預設情況下,空白必須精確匹配。NORMALIZE_WHITESPACE 在預期輸出的一行非常長,並且你希望將其分成多行顯示在原始碼中時特別有用。

doctest.ELLIPSIS

當指定時,預期輸出中的省略號標記(...)可以匹配實際輸出中的任何子字串。這包括跨越行邊界的子字串和空子字串,所以最好簡單地使用它。複雜的使用方式可能會導致與正則表示式中 .* 容易出現的“哎呀,匹配得太多了!”的意外情況類似。

doctest.IGNORE_EXCEPTION_DETAIL

當指定時,期望異常的 doctest 只要引發了預期型別的異常就會透過,即使詳細資訊(訊息和完全限定的異常名稱)不匹配。

例如,一個期望 ValueError: 42 的示例,如果實際引發的異常是 ValueError: 3*14 就會透過,但如果引發了 TypeError 就會失敗。它還會忽略異常類之前的任何完全限定名稱,這些名稱在不同的實現、Python 版本以及使用的程式碼/庫之間可能會有所不同。因此,在使用此標誌的情況下,以下三種變體都將起作用:

>>> raise Exception('message')
Traceback (most recent call last):
Exception: message

>>> raise Exception('message')
Traceback (most recent call last):
builtins.Exception: message

>>> raise Exception('message')
Traceback (most recent call last):
__main__.Exception: message

請注意,ELLIPSIS 也可以用來忽略異常訊息的細節,但這樣的測試仍可能因為模組名稱是否存在或是否完全匹配而失敗。

在 3.2 版更改: IGNORE_EXCEPTION_DETAIL 現在也會忽略與被測試異常所在模組相關的任何資訊。

doctest.SKIP

當指定時,完全不執行該示例。這在 doctest 示例既作為文件又作為測試用例,並且某個示例應出於文件目的被包含,但不應被檢查的情況下非常有用。例如,示例的輸出可能是隨機的;或者示例可能依賴於測試驅動程式不可用的資源。

SKIP 標誌也可以用於臨時“註釋掉”示例。

doctest.COMPARISON_FLAGS

一個位掩碼,將上述所有比較標誌進行“或”運算組合在一起。

第二組選項控制如何報告測試失敗:

doctest.REPORT_UDIFF

當指定時,涉及多行預期和實際輸出的失敗將使用統一差異(unified diff)格式顯示。

doctest.REPORT_CDIFF

當指定時,涉及多行預期和實際輸出的失敗將使用上下文差異(context diff)格式顯示。

doctest.REPORT_NDIFF

當指定時,差異由 difflib.Differ 計算,使用與流行的 ndiff.py 工具相同的演算法。這是唯一一種既能標記行內差異又能標記跨行差異的方法。例如,如果一行預期輸出包含數字 1,而實際輸出包含字母 l,則會插入一行,用一個插入符號標記不匹配的列位置。

doctest.REPORT_ONLY_FIRST_FAILURE

當指定時,顯示每個 doctest 中的第一個失敗示例,但抑制所有其餘示例的輸出。這將防止 doctest 報告因早期失敗而中斷的正確示例;但它也可能隱藏獨立於第一個失敗而失敗的不正確示例。當指定 REPORT_ONLY_FIRST_FAILURE 時,其餘示例仍然會執行,並計入報告的總失敗數中;只有輸出被抑制。

doctest.FAIL_FAST

當指定時,在第一個失敗的示例後退出,不嘗試執行其餘的示例。因此,報告的失敗數最多為1。這個標誌在除錯時可能很有用,因為第一個失敗後的示例甚至不會產生除錯輸出。

doctest.REPORTING_FLAGS

一個位掩碼,將上述所有報告標誌進行“或”運算組合在一起。

還有一種方法可以註冊新的選項標誌名稱,但這除非你打算透過子類化來擴充套件 doctest 的內部機制,否則用處不大:

doctest.register_optionflag(name)

使用給定的名稱建立一個新的選項標誌,並返回新標誌的整數值。register_optionflag() 可以在子類化 OutputCheckerDocTestRunner 時使用,以建立你的子類支援的新選項。register_optionflag() 應始終使用以下慣用法呼叫:

MY_FLAG = register_optionflag('MY_FLAG')

指令

Doctest 指令可用於修改單個示例的選項標誌。Doctest 指令是跟在示例原始碼後面的特殊 Python 註釋:

directive:             "#" "doctest:" directive_options
directive_options:     directive_option ("," directive_option)*
directive_option:      on_or_off directive_option_name
on_or_off:             "+" | "-"
directive_option_name: "DONT_ACCEPT_BLANKLINE" | "NORMALIZE_WHITESPACE" | ...

+- 與指令選項名稱之間不允許有空格。指令選項名稱可以是上面解釋的任何選項標誌名稱。

一個示例的 doctest 指令會修改 doctest 對該單個示例的行為。使用 + 來啟用指定的行為,或使用 - 來停用它。

例如,這個測試會透過:

>>> print(list(range(20)))  # doctest: +NORMALIZE_WHITESPACE
[0,   1,  2,  3,  4,  5,  6,  7,  8,  9,
10,  11, 12, 13, 14, 15, 16, 17, 18, 19]

如果沒有這個指令,它會失敗,因為實際輸出在單位數列表元素前沒有兩個空格,並且實際輸出在單行上。這個測試也會透過,並且也需要一個指令才能這樣做:

>>> print(list(range(20)))  # doctest: +ELLIPSIS
[0, 1, ..., 18, 19]

可以在單個物理行上使用多個指令,用逗號分隔:

>>> print(list(range(20)))  # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE
[0,    1, ...,   18,    19]

如果單個示例使用了多個指令註釋,那麼它們會被合併:

>>> print(list(range(20)))  # doctest: +ELLIPSIS
...                         # doctest: +NORMALIZE_WHITESPACE
[0,    1, ...,   18,    19]

如前一個示例所示,你可以向示例中新增只包含指令的 ... 行。當一個示例太長,指令無法舒適地放在同一行時,這可能很有用:

>>> print(list(range(5)) + list(range(10, 20)) + list(range(30, 40)))
... # doctest: +ELLIPSIS
[0, ..., 4, 10, ..., 19, 30, ..., 39]

請注意,由於所有選項預設都是停用的,並且指令僅適用於它們所在的示例,因此啟用選項(透過指令中的 +)通常是唯一有意義的選擇。但是,選項標誌也可以傳遞給執行 doctest 的函式,從而建立不同的預設值。在這種情況下,透過指令中的 - 停用一個選項可能很有用。

警告

doctest 對預期輸出的精確匹配要求非常嚴格。即使只有一個字元不匹配,測試也會失敗。在你學習 Python 對輸出的保證和不保證的確切內容時,這可能會讓你驚訝幾次。例如,列印一個集合時,Python 不保證元素以任何特定順序列印,所以像這樣的測試:

>>> foo()
{"spam", "eggs"}

是脆弱的!一種解決方法是這樣做:

>>> foo() == {"spam", "eggs"}
True

另一種方法是:

>>> d = sorted(foo())
>>> d
['eggs', 'spam']

還有其他方法,但你應該明白這個意思了。

另一個不好的想法是列印嵌入了物件地址的東西,比如:

>>> id(1.0)  # certain to fail some of the time
7948648
>>> class C: pass
>>> C()  # the default repr() for instances embeds an address
<C object at 0x00AC18F0>

ELLIPSIS 指令為最後一個例子提供了一個很好的方法:

>>> C()  # doctest: +ELLIPSIS
<C object at 0x...>

浮點數在不同平臺上的輸出也可能存在微小差異,因為 Python 依賴於平臺的 C 庫進行一些浮點數計算,而 C 庫在這方面的質量差異很大。

>>> 1000**0.1  # risky
1.9952623149688797
>>> round(1000**0.1, 9) # safer
1.995262315
>>> print(f'{1000**0.1:.4f}') # much safer
1.9953

形如 I/2.**J 的數字在所有平臺上都是安全的,我經常設計 doctest 示例來產生這種形式的數字:

>>> 3./4  # utterly safe
0.75

簡單的分數也更容易讓人理解,這使得文件更好。

基本 API

函式 testmod()testfile() 提供了 doctest 的一個簡單介面,對於大多數基本用途來說應該足夠了。關於這兩個函式的非正式介紹,請參見 簡單用法:檢查文件字串中的示例簡單用法:檢查文字檔案中的示例 部分。

doctest.testfile(filename, module_relative=True, name=None, package=None, globs=None, verbose=None, report=True, optionflags=0, extraglobs=None, raise_on_error=False, parser=DocTestParser(), encoding=None)

除了 *filename* 外,所有引數都是可選的,並且應該以關鍵字形式指定。

測試名為 *filename* 的檔案中的示例。返回 (failure_count, test_count)

可選引數 *module_relative* 指定了如何解釋檔名:

  • 如果 *module_relative* 是 True(預設值),那麼 *filename* 指定一個與作業系統無關的、相對於模組的路徑。預設情況下,此路徑相對於呼叫模組的目錄;但如果指定了 *package* 引數,則它相對於該包。為了確保與作業系統無關,*filename* 應使用 / 字元來分隔路徑段,並且不能是絕對路徑(即,不能以 / 開頭)。

  • 如果 *module_relative* 為 False,則 *filename* 指定一個特定於作業系統的路徑。路徑可以是絕對的或相對的;相對路徑是相對於當前工作目錄解析的。

可選引數 *name* 給出測試的名稱;預設情況下,或如果為 None,則使用 os.path.basename(filename)

可選引數 *package* 是一個 Python 包或 Python 包的名稱,其目錄應用作模組相對檔名的基目錄。如果沒有指定包,則呼叫模組的目錄將用作模組相對檔名的基目錄。如果 *module_relative* 為 False,則指定 *package* 是一個錯誤。

可選引數 *globs* 提供一個字典,用作執行示例時的全域性變數。會為 doctest 建立此字典的一個新的淺複製,因此其示例以一個乾淨的狀態開始。預設情況下,或如果為 None,則使用一個新的空字典。

可選引數 *extraglobs* 提供了一個合併到用於執行示例的全域性變數中的字典。這類似於 dict.update():如果 *globs* 和 *extraglobs* 有一個共同的鍵,那麼 *extraglobs* 中關聯的值會出現在合併後的字典中。預設情況下,或如果為 None,則不使用額外的全域性變數。這是一個高階功能,允許對 doctest 進行引數化。例如,可以為一個基類編寫 doctest,使用一個通用的類名,然後透過傳遞一個將通用名稱對映到要測試的子類的 *extraglobs* 字典來重用它來測試任意數量的子類。

可選引數 *verbose* 如果為真,則列印大量資訊,如果為假,則僅列印失敗資訊;預設情況下,或如果為 None,當且僅當 '-v'sys.argv 中時為真。

可選引數 *report* 在為真時在末尾列印摘要,否則在末尾不列印任何內容。在詳細模式下,摘要是詳細的,否則摘要非常簡短(事實上,如果所有測試都透過,則為空)。

可選引數 *optionflags*(預設值 0)接受選項標誌的按位或。請參見 選項標誌 部分。

可選引數 *raise_on_error* 預設為 false。如果為 true,則在示例中出現第一個失敗或意外異常時會引發異常。這允許對失敗進行事後除錯。預設行為是繼續執行示例。

可選引數 *parser* 指定一個 DocTestParser(或其子類),用於從檔案中提取測試。它預設為一個普通的解析器(即 DocTestParser())。

可選引數 *encoding* 指定了一個應在將檔案轉換為 unicode 時使用的編碼。

doctest.testmod(m=None, name=None, globs=None, verbose=None, report=True, optionflags=0, extraglobs=None, raise_on_error=False, exclude_empty=False)

所有引數都是可選的,除了 *m* 之外,都應以關鍵字形式指定。

測試從模組 *m*(或模組 __main__,如果 *m* 未提供或為 None)可達的函式和類中的文件字串中的示例,從 m.__doc__ 開始。

如果存在,也測試從字典 m.__test__ 可達的示例。m.__test__ 將名稱(字串)對映到函式、類和字串;函式和類的文件字串會被搜尋示例;字串則直接被搜尋,就像它們是文件字串一樣。

只搜尋附加到屬於模組 *m* 的物件的文件字串。

返回 (failure_count, test_count)

可選引數 *name* 給出模組的名稱;預設情況下,或如果為 None,則使用 m.__name__

可選引數 *exclude_empty* 預設為 false。如果為 true,則未找到 doctest 的物件將從考慮中排除。預設值是一個向後相容的技巧,以便仍在結合 testmod() 使用 doctest.master.summarize 的程式碼繼續為沒有測試的物件獲取輸出。較新的 DocTestFinder 建構函式的 *exclude_empty* 引數預設為 true。

可選引數 *extraglobs*、*verbose*、*report*、*optionflags*、*raise_on_error* 和 *globs* 與上面的函式 testfile() 相同,只是 *globs* 預設為 m.__dict__

doctest.run_docstring_examples(f, globs, verbose=False, name='NoName', compileflags=None, optionflags=0)

測試與物件 *f* 關聯的示例;例如,*f* 可以是字串、模組、函式或類物件。

字典引數 *globs* 的淺複製用作執行上下文。

可選引數 *name* 用於失敗訊息,預設為 "NoName"

如果可選引數 *verbose* 為真,即使沒有失敗也會生成輸出。預設情況下,僅在示例失敗時生成輸出。

可選引數 *compileflags* 給出在執行示例時 Python 編譯器應使用的一組標誌。預設情況下,或如果為 None,則根據在 *globs* 中找到的未來特性集推斷標誌。

可選引數 *optionflags* 的作用與上面的函式 testfile() 相同。

Unittest API

隨著你收集的 doctest 模組越來越多,你會想要一種系統地執行所有 doctest 的方法。doctest 提供了兩個函式,可用於從包含 doctest 的模組和文字檔案中建立 unittest 測試套件。要與 unittest 測試發現整合,請在你的測試模組中包含一個 load_tests 函式:

import unittest
import doctest
import my_module_with_doctests

def load_tests(loader, tests, ignore):
    tests.addTests(doctest.DocTestSuite(my_module_with_doctests))
    return tests

有兩個主要函式用於從包含 doctest 的文字檔案和模組建立 unittest.TestSuite 例項:

doctest.DocFileSuite(*paths, module_relative=True, package=None, setUp=None, tearDown=None, globs=None, optionflags=0, parser=DocTestParser(), encoding=None)

將一個或多個文字檔案中的 doctest 測試轉換為 unittest.TestSuite

返回的 unittest.TestSuite 將由 unittest 框架執行,並執行每個檔案中的互動式示例。如果任何檔案中的示例失敗,則合成的單元測試失敗,並引發一個 failureException 異常,顯示包含測試的檔名和(有時是近似的)行號。如果一個檔案中的所有示例都被跳過,則合成的單元測試也被標記為跳過。

傳遞一個或多個要檢查的文字檔案的路徑(作為字串)。

選項可以作為關鍵字引數提供:

可選引數 *module_relative* 指定如何解釋 *paths* 中的檔名:

  • 如果 *module_relative* 是 True(預設值),那麼 *paths* 中的每個檔名都指定一個與作業系統無關的、相對於模組的路徑。預設情況下,此路徑相對於呼叫模組的目錄;但如果指定了 *package* 引數,則它相對於該包。為了確保與作業系統無關,每個檔名應使用 / 字元來分隔路徑段,並且不能是絕對路徑(即,不能以 / 開頭)。

  • 如果 *module_relative* 為 False,則 *paths* 中的每個檔名都指定一個特定於作業系統的路徑。路徑可以是絕對的或相對的;相對路徑是相對於當前工作目錄解析的。

可選引數 *package* 是一個 Python 包或 Python 包的名稱,其目錄應用作 *paths* 中模組相對檔名的基目錄。如果沒有指定包,則呼叫模組的目錄將用作模組相對檔名的基目錄。如果 *module_relative* 為 False,則指定 *package* 是一個錯誤。

可選引數 *setUp* 為測試套件指定一個設定函式。它在執行每個檔案中的測試之前被呼叫。*setUp* 函式將被傳遞一個 DocTest 物件。*setUp* 函式可以訪問測試的全域性變數,即傳遞的測試物件的 globs 屬性。

可選引數 *tearDown* 為測試套件指定一個拆卸函式。它在執行每個檔案中的測試之後被呼叫。*tearDown* 函式將被傳遞一個 DocTest 物件。*tearDown* 函式可以訪問測試的全域性變數,即傳遞的測試物件的 globs 屬性。

可選引數 *globs* 是一個包含測試初始全域性變數的字典。會為每個測試建立這個字典的一個新副本。預設情況下,*globs* 是一個新的空字典。

可選引數 *optionflags* 指定測試的預設 doctest 選項,透過將單個選項標誌進行或運算建立。請參見 選項標誌 部分。關於設定報告選項的更好方法,請參見下面的 set_unittest_reportflags() 函式。

可選引數 *parser* 指定一個 DocTestParser(或其子類),用於從檔案中提取測試。它預設為一個普通的解析器(即 DocTestParser())。

可選引數 *encoding* 指定了一個應在將檔案轉換為 unicode 時使用的編碼。

全域性變數 __file__ 會被新增到使用 DocFileSuite() 從文字檔案載入的 doctest 的全域性變數中。

doctest.DocTestSuite(module=None, globs=None, extraglobs=None, test_finder=None, setUp=None, tearDown=None, optionflags=0, checker=None)

將一個模組的 doctest 測試轉換為一個 unittest.TestSuite

返回的 unittest.TestSuite 將由 unittest 框架執行,並執行模組中的每個 doctest。每個文件字串作為單獨的單元測試執行。如果任何 doctest 失敗,則合成的單元測試失敗,並引發一個 unittest.TestCase.failureException 異常,顯示包含測試的檔名和(有時是近似的)行號。如果一個文件字串中的所有示例都被跳過,則

可選引數 *module* 提供要測試的模組。它可以是一個模組物件或一個(可能是帶點的)模組名稱。如果未指定,則使用呼叫此函式的模組。

可選引數 *globs* 是一個包含測試初始全域性變數的字典。會為每個測試建立這個字典的一個新副本。預設情況下,*globs* 是模組的 __dict__

可選引數 *extraglobs* 指定一組額外的全域性變數,它會合併到 *globs* 中。預設情況下,不使用額外的全域性變數。

可選引數 *test_finder* 是用於從模組中提取 doctest 的 DocTestFinder 物件(或其替代品)。

可選引數 setUptearDownoptionflags 與上面的函式 DocFileSuite() 相同,但它們是為每個文件字串呼叫的。

此函式使用與 testmod() 相同的搜尋技術。

在 3.5 版更改: 如果 module 不包含文件字串,DocTestSuite() 會返回一個空的 unittest.TestSuite,而不是引發 ValueError

在底層,DocTestSuite()doctest.DocTestCase 例項建立了一個 unittest.TestSuite,而 DocTestCaseunittest.TestCase 的子類。這裡沒有記錄 DocTestCase(它是一個內部細節),但研究其程式碼可以回答有關 unittest 整合確切細節的問題。

同樣,DocFileSuite()doctest.DocFileCase 例項建立一個 unittest.TestSuite,而 DocFileCaseDocTestCase 的子類。

因此,建立 unittest.TestSuite 的兩種方式都執行 DocTestCase 的例項。這有一個微妙的重要原因:當您自己執行 doctest 函式時,可以透過向 doctest 函式傳遞選項標誌來直接控制正在使用的 doctest 選項。但是,如果您正在編寫一個 unittest 框架,unittest 最終控制著測試的執行時間和方式。框架作者通常希望控制 doctest 的報告選項(例如,可能由命令列選項指定),但沒有辦法透過 unittest 將選項傳遞給 doctest 的測試執行器。

因此,doctest 還透過此函式支援特定於 unittest 支援的 doctest 報告標誌的概念

doctest.set_unittest_reportflags(flags)

設定要使用的 doctest 報告標誌。

引數 flags 接受選項標誌的按位或。請參閱 選項標誌 部分。只能使用“報告標誌”。

這是一個模組全域性設定,會影響 unittest 模組執行的所有未來 doctest:DocTestCaserunTest() 方法會檢視構造 DocTestCase 例項時為測試用例指定的選項標誌。如果未指定報告標誌(這是典型和預期的情況),doctestunittest 報告標誌會按位或到選項標誌中,然後將如此增強的選項標誌傳遞給為執行 doctest 而建立的 DocTestRunner 例項。如果在構造 DocTestCase 例項時指定了任何報告標誌,則會忽略 doctestunittest 報告標誌。

該函式會返回在呼叫函式之前有效的 unittest 報告標誌的值。

高階 API

基本 API 是一個簡單的包裝器,旨在使 doctest 易於使用。它相當靈活,應該能滿足大多數使用者的需求;但是,如果您需要對測試進行更精細的控制,或希望擴充套件 doctest 的功能,則應使用高階 API。

高階 API 圍繞兩個容器類構建,它們用於儲存從 doctest 用例中提取的互動式示例

  • Example:單個 Python 語句,與其預期輸出配對。

  • DocTestExample 的集合,通常從單個文件字串或文字檔案中提取。

定義了額外的處理類來查詢、解析、執行和檢查 doctest 示例

這些處理類之間的關係總結在下圖中

                            list of:
+------+                   +---------+
|module| --DocTestFinder-> | DocTest | --DocTestRunner-> results
+------+    |        ^     +---------+     |       ^    (printed)
            |        |     | Example |     |       |
            v        |     |   ...   |     v       |
           DocTestParser   | Example |   OutputChecker
                           +---------+

DocTest 物件

class doctest.DocTest(examples, globs, name, filename, lineno, docstring)

應在單個名稱空間中執行的 doctest 示例的集合。建構函式引數用於初始化同名屬性。

DocTest 定義了以下屬性。它們由建構函式初始化,不應直接修改。

examples

一個 Example 物件列表,編碼了此測試應執行的各個互動式 Python 示例。

globs

示例應在其中執行的名稱空間(也稱為全域性變數)。這是一個將名稱對映到值的字典。示例對名稱空間所做的任何更改(例如繫結新變數)都將在測試執行後反映在 globs 中。

name

標識 DocTest 的字串名稱。通常,這是從中提取測試的物件或檔案的名稱。

filename

從中提取此 DocTest 的檔案的名稱;如果檔名未知,或者 DocTest 不是從檔案中提取的,則為 None

lineno

DocTestfilename 內開始的行號;如果行號不可用,則為 None。此行號是相對於檔案開頭的零基行號。

docstring

從中提取測試的字串;如果字串不可用,或者測試不是從字串中提取的,則為 None

Example 物件

class doctest.Example(source, want, exc_msg=None, lineno=0, indent=0, options=None)

單個互動式示例,由一個 Python 語句及其預期輸出組成。建構函式引數用於初始化同名屬性。

Example 定義了以下屬性。它們由建構函式初始化,不應直接修改。

source

包含示例原始碼的字串。此原始碼由單個 Python 語句組成,並始終以換行符結尾;建構函式在必要時會新增換行符。

want

執行示例原始碼的預期輸出(來自 stdout,或在異常情況下的回溯資訊)。除非沒有預期輸出,否則 want 以換行符結尾,在這種情況下,它是一個空字串。建構函式在必要時會新增換行符。

exc_msg

如果示例預期生成異常,則為該示例生成的異常訊息;如果不預期生成異常,則為 None。此異常訊息與 traceback.format_exception_only() 的返回值進行比較。exc_msg 以換行符結尾,除非它是 None。建構函式在需要時會新增換行符。

lineno

在包含此示例的字串中,該示例開始的行號。此行號是相對於包含字串開頭的零基行號。

indent

示例在包含字串中的縮排,即示例第一個提示符之前的空格字元數。

options

一個從選項標誌對映到 TrueFalse 的字典,用於為此示例覆蓋預設選項。此字典中未包含的任何選項標誌都保留其預設值(由 DocTestRunneroptionflags 指定)。預設情況下,不設定任何選項。

DocTestFinder 物件

class doctest.DocTestFinder(verbose=False, parser=DocTestParser(), recurse=True, exclude_empty=True)

一個處理類,用於從給定物件的文件字串及其包含物件的文件字串中提取相關的 DocTests。DocTests 可以從模組、類、函式、方法、靜態方法、類方法和屬性中提取。

可選引數 verbose 可用於顯示查詢器搜尋的物件。它預設為 False(無輸出)。

可選引數 parser 指定用於從文件字串中提取 doctest 的 DocTestParser 物件(或其替代品)。

如果可選引數 recurse 為 false,則 DocTestFinder.find() 將只檢查給定的物件,而不檢查任何包含的物件。

如果可選引數 exclude_empty 為 false,則 DocTestFinder.find() 將包含文件字串為空的物件的測試。

DocTestFinder 定義了以下方法

find(obj[, name][, module][, globs][, extraglobs])

返回由 obj 的文件字串或其任何包含物件的文件字串定義的 DocTests 列表。

可選引數 name 指定物件的名稱;此名稱將用於構造返回的 DocTests 的名稱。如果未指定 name,則使用 obj.__name__

可選引數 module 是包含給定物件的模組。如果未指定模組或是 None,則測試查詢器將嘗試自動確定正確的模組。物件的模組用於

  • 作為預設名稱空間,如果未指定 globs

  • 防止 DocTestFinder 從其他模組匯入的物件中提取 DocTests。(模組不同於 module 的包含物件將被忽略。)

  • 查詢包含物件的檔名。

  • 幫助在其檔案中查詢物件的行號。

如果 moduleFalse,則不會嘗試查詢模組。這很晦澀,主要用於測試 doctest 本身:如果 moduleFalse,或是 None 但無法自動找到,則所有物件都被認為屬於(不存在的)模組,因此將(遞迴地)搜尋所有包含的物件以查詢 doctest。

每個 DocTest 的全域性變數是透過組合 globsextraglobs 形成的(extraglobs 中的繫結會覆蓋 globs 中的繫結)。為每個 DocTest 建立一個新的全域性字典淺複製。如果未指定 globs,則預設為模組的 __dict__(如果已指定),否則為 {}。如果未指定 extraglobs,則預設為 {}

DocTestParser 物件

class doctest.DocTestParser

一個處理類,用於從字串中提取互動式示例,並使用它們建立一個 DocTest 物件。

DocTestParser 定義了以下方法

get_doctest(string, globs, name, filename, lineno)

從給定字串中提取所有 doctest 示例,並將它們收集到一個 DocTest 物件中。

globsnamefilenamelineno 是新的 DocTest 物件的屬性。有關更多資訊,請參閱 DocTest 的文件。

get_examples(string, name='<string>')

從給定字串中提取所有 doctest 示例,並將它們作為 Example 物件列表返回。行號是 0-based 的。可選引數 name 是標識此字串的名稱,僅用於錯誤訊息。

parse(string, name='<string>')

將給定字串劃分為示例和中間文字,並將它們作為交替的 Examples 和字串列表返回。Examples 的行號是 0-based 的。可選引數 name 是標識此字串的名稱,僅用於錯誤訊息。

TestResults 物件

class doctest.TestResults(failed, attempted)
failed

失敗的測試數。

attempted

嘗試的測試數。

skipped

跳過的測試數。

在 3.13 版本加入。

DocTestRunner 物件

class doctest.DocTestRunner(checker=None, verbose=None, optionflags=0)

一個處理類,用於執行和驗證 DocTest 中的互動式示例。

預期輸出和實際輸出之間的比較由 OutputChecker 完成。可以使用多個選項標誌來自定義此比較;有關更多資訊,請參閱 選項標誌 部分。如果選項標誌不足,也可以透過向建構函式傳遞 OutputChecker 的子類來自定義比較。

測試執行器的顯示輸出可以透過兩種方式控制。首先,可以將輸出函式傳遞給 run();此函式將使用應顯示的字串呼叫。它預設為 sys.stdout.write。如果捕獲輸出不足,也可以透過子類化 DocTestRunner 並覆蓋 report_start()report_success()report_unexpected_exception()report_failure() 方法來自定義顯示輸出。

可選關鍵字引數 checker 指定應用於比較 doctest 示例的預期輸出與實際輸出的 OutputChecker 物件(或其替代品)。

可選關鍵字引數 verbose 控制 DocTestRunner 的詳細程度。如果 verboseTrue,則在執行時會列印有關每個示例的資訊。如果 verboseFalse,則只打印失敗資訊。如果未指定 verbose 或為 None,則僅當使用命令列開關 -v 時才使用詳細輸出。

可選關鍵字引數 optionflags 可用於控制測試執行器如何比較預期輸出與實際輸出,以及如何顯示失敗。有關更多資訊,請參閱 選項標誌 部分。

測試執行器會累積統計資訊。嘗試、失敗和跳過的示例的總數也可以透過 triesfailuresskips 屬性獲得。run()summarize() 方法返回一個 TestResults 例項。

DocTestRunner 定義了以下方法

report_start(out, test, example)

報告測試執行器即將處理給定的示例。提供此方法是為了允許 DocTestRunner 的子類自定義其輸出;不應直接呼叫它。

example 是即將處理的示例。test 是包含 example 的測試。out 是傳遞給 DocTestRunner.run() 的輸出函式。

report_success(out, test, example, got)

報告給定的示例成功執行。提供此方法是為了允許 DocTestRunner 的子類自定義其輸出;不應直接呼叫它。

example 是即將處理的示例。got 是示例的實際輸出。test 是包含 example 的測試。out 是傳遞給 DocTestRunner.run() 的輸出函式。

report_failure(out, test, example, got)

報告給定的示例失敗。提供此方法是為了允許 DocTestRunner 的子類自定義其輸出;不應直接呼叫它。

example 是即將處理的示例。got 是示例的實際輸出。test 是包含 example 的測試。out 是傳遞給 DocTestRunner.run() 的輸出函式。

report_unexpected_exception(out, test, example, exc_info)

報告給定的示例引發了意外的異常。提供此方法是為了允許 DocTestRunner 的子類自定義其輸出;不應直接呼叫它。

example 是即將處理的示例。exc_info 是一個包含有關意外異常資訊的元組(由 sys.exc_info() 返回)。test 是包含 example 的測試。out 是傳遞給 DocTestRunner.run() 的輸出函式。

run(test, compileflags=None, out=None, clear_globs=True)

執行 test(一個 DocTest 物件)中的示例,並使用寫入器函式 out 顯示結果。返回一個 TestResults 例項。

示例在名稱空間 test.globs 中執行。如果 clear_globs 為 true(預設值),則此名稱空間將在測試執行後被清除,以幫助進行垃圾回收。如果您想在測試完成後檢查名稱空間,則使用 clear_globs=False

compileflags 給出執行示例時 Python 編譯器應使用的標誌集。如果未指定,則預設為適用於 globs 的 future-import 標誌集。

每個示例的輸出都使用 DocTestRunner 的輸出檢查器進行檢查,結果由 DocTestRunner.report_*() 方法格式化。

summarize(verbose=None)

列印此 DocTestRunner 已執行的所有測試用例的摘要,並返回一個 TestResults 例項。

可選的 verbose 引數控制摘要的詳細程度。如果未指定詳細程度,則使用 DocTestRunner 的詳細程度。

DocTestParser 具有以下屬性

tries

嘗試的示例數。

failures

失敗的示例數。

skips

跳過的示例數。

在 3.13 版本加入。

OutputChecker 物件

class doctest.OutputChecker

用於檢查 doctest 示例的實際輸出是否與預期輸出匹配的類。OutputChecker 定義了兩種方法:check_output(),它比較給定的一對輸出,如果它們匹配則返回 True;以及 output_difference(),它返回一個描述兩個輸出之間差異的字串。

OutputChecker 定義了以下方法

check_output(want, got, optionflags)

當且僅當示例的實際輸出(got)與預期輸出(want)匹配時返回 True。如果這些字串完全相同,則始終認為它們匹配;但根據測試執行器使用的選項標誌,也可能有幾種非精確匹配型別。有關選項標誌的更多資訊,請參閱 選項標誌 部分。

output_difference(example, got, optionflags)

返回一個字串,描述給定示例的預期輸出(example)與實際輸出(got)之間的差異。optionflags 是用於比較 wantgot 的選項標誌集。

除錯

Doctest 提供了幾種除錯 doctest 示例的機制

  • 有幾個函式可以將 doctest 轉換為可執行的 Python 程式,這些程式可以在 Python 偵錯程式 pdb 下執行。

  • DebugRunner 類是 DocTestRunner 的一個子類,它會為第一個失敗的示例引發一個異常,其中包含有關該示例的資訊。此資訊可用於對該示例進行事後除錯。

  • DocTestSuite() 生成的 unittest 用例支援 unittest.TestCase 定義的 debug() 方法。

  • 您可以在 doctest 示例中新增對 pdb.set_trace() 的呼叫,當該行被執行時,您將進入 Python 偵錯程式。然後您可以檢查變數的當前值等等。例如,假設 a.py 只包含這個模組文件字串

    """
    >>> def f(x):
    ...     g(x*2)
    >>> def g(x):
    ...     print(x+3)
    ...     import pdb; pdb.set_trace()
    >>> f(3)
    9
    """
    

    那麼一個互動式 Python 會話可能看起來像這樣

    >>> import a, doctest
    >>> doctest.testmod(a)
    --Return--
    > <doctest a[1]>(3)g()->None
    -> import pdb; pdb.set_trace()
    (Pdb) list
      1     def g(x):
      2         print(x+3)
      3  ->     import pdb; pdb.set_trace()
    [EOF]
    (Pdb) p x
    6
    (Pdb) step
    --Return--
    > <doctest a[0]>(2)f()->None
    -> g(x*2)
    (Pdb) list
      1     def f(x):
      2  ->     g(x*2)
    [EOF]
    (Pdb) p x
    3
    (Pdb) step
    --Return--
    > <doctest a[2]>(1)?()->None
    -> f(3)
    (Pdb) cont
    (0, 3)
    >>>
    

將 doctest 轉換為 Python 程式碼,並可能在偵錯程式下執行合成程式碼的函式

doctest.script_from_examples(s)

將帶有示例的文字轉換為指令碼。

引數 s 是一個包含 doctest 示例的字串。該字串被轉換為一個 Python 指令碼,其中 s 中的 doctest 示例被轉換為常規程式碼,其他所有內容都被轉換為 Python 註釋。生成的指令碼作為字串返回。例如,

import doctest
print(doctest.script_from_examples(r"""
    Set x and y to 1 and 2.
    >>> x, y = 1, 2

    Print their sum:
    >>> print(x+y)
    3
"""))

顯示

# Set x and y to 1 and 2.
x, y = 1, 2
#
# Print their sum:
print(x+y)
# Expected:
## 3

此函式由其他函式(見下文)在內部使用,但當您想將互動式 Python 會話轉換為 Python 指令碼時也很有用。

doctest.testsource(module, name)

將物件的 doctest 轉換為指令碼。

引數 module 是一個模組物件,或包含感興趣的 doctest 物件的模組的點分名稱。引數 name 是(模組內)具有感興趣的 doctest 的物件的名稱。結果是一個字串,包含物件的文件字串轉換為 Python 指令碼,如上文 script_from_examples() 所述。例如,如果模組 a.py 包含一個頂層函式 f(),那麼

import a, doctest
print(doctest.testsource(a, "a.f"))

列印函式 f() 的文件字串的指令碼版本,其中 doctest 轉換為程式碼,其餘部分放在註釋中。

doctest.debug(module, name, pm=False)

除錯物件的 doctest。

modulename 引數與上面的函式 testsource() 相同。為命名物件的文件字串合成的 Python 指令碼被寫入一個臨時檔案,然後該檔案在 Python 偵錯程式 pdb 的控制下執行。

module.__dict__ 的淺複製用於本地和全域性執行上下文。

可選引數 pm 控制是否使用事後除錯。如果 pm 具有真值,則直接執行指令碼檔案,並且只有在指令碼透過引發未處理的異常終止時,偵錯程式才會介入。如果發生這種情況,則透過 pdb.post_mortem() 呼叫事後除錯,並傳遞來自未處理異常的回溯物件。如果未指定 pm 或為 false,則從一開始就在偵錯程式下執行指令碼,透過將適當的 exec() 呼叫傳遞給 pdb.run()

doctest.debug_src(src, pm=False, globs=None)

除錯字串中的 doctest。

這類似於上面的函式 debug(),只是直接透過 src 引數指定包含 doctest 示例的字串。

可選引數 pm 與上面的函式 debug() 中的含義相同。

可選引數 globs 給出一個字典,用作本地和全域性執行上下文。如果未指定或為 None,則使用空字典。如果指定,則使用該字典的淺複製。

DebugRunner 類及其可能引發的特殊異常,對測試框架作者最感興趣,這裡只做簡要介紹。有關更多詳細資訊,請參閱原始碼,尤其是 DebugRunner 的文件字串(它是一個 doctest!)

class doctest.DebugRunner(checker=None, verbose=None, optionflags=0)

DocTestRunner 的一個子類,它在遇到失敗時立即引發異常。如果發生意外異常,則會引發 UnexpectedException 異常,其中包含測試、示例和原始異常。如果輸出不匹配,則會引發 DocTestFailure 異常,其中包含測試、示例和實際輸出。

有關建構函式引數和方法的資訊,請參閱 高階 API 部分中 DocTestRunner 的文件。

DebugRunner 例項可能引發兩種異常

exception doctest.DocTestFailure(test, example, got)

DocTestRunner 引發的異常,表示 doctest 示例的實際輸出與其預期輸出不匹配。建構函式引數用於初始化同名屬性。

DocTestFailure 定義了以下屬性

DocTestFailure.test

示例失敗時正在執行的 DocTest 物件。

DocTestFailure.example

失敗的 Example

DocTestFailure.got

示例的實際輸出。

exception doctest.UnexpectedException(test, example, exc_info)

DocTestRunner 引發的異常,表示 doctest 示例引發了意外的異常。建構函式引數用於初始化同名屬性。

UnexpectedException 定義了以下屬性

UnexpectedException.test

示例失敗時正在執行的 DocTest 物件。

UnexpectedException.example

失敗的 Example

UnexpectedException.exc_info

一個包含有關意外異常資訊的元組,由 sys.exc_info() 返回。

Soapbox

如引言中所述,doctest 已發展出三種主要用途

  1. 檢查文件字串中的示例。

  2. 迴歸測試。

  3. 可執行文件/文學測試。

這些用途有不同的要求,區分它們很重要。特別是,用晦澀的測試用例填充文件字串會產生糟糕的文件。

在編寫文件字串時,請謹慎選擇文件字串示例。這是一門需要學習的藝術——起初可能不自然。示例應該為文件增加真正的價值。一個好的示例通常勝過千言萬語。如果做得仔細,這些示例對您的使用者將是無價之寶,並且隨著歲月流逝和事物變化,將無數倍地回報您收集它們所花費的時間。我仍然驚訝於我的一個 doctest 示例在一次“無害”的更改後停止工作的頻率。

Doctest 也是一個出色的迴歸測試工具,尤其是如果您不吝嗇解釋性文字。透過將散文和示例交織在一起,跟蹤實際測試的內容和原因變得容易得多。當測試失敗時,好的散文可以使找出問題所在以及如何修復它變得容易得多。誠然,您可以在基於程式碼的測試中編寫大量註釋,但很少有程式設計師這樣做。許多人發現,使用 doctest 方法可以產生更清晰的測試。也許這僅僅是因為 doctest 使編寫散文比編寫程式碼更容易一些,而在程式碼中編寫註釋則更難一些。我認為這比這更深入:編寫基於 doctest 的測試時的自然態度是,您想要解釋軟體的精妙之處,並用示例來說明它們。這反過來又自然地導致測試檔案從最簡單的功能開始,然後邏輯地進展到複雜性和邊緣情況。結果是一個連貫的敘述,而不是一個似乎隨機測試孤立功能位的孤立函式集合。這是一種不同的態度,產生不同的結果,模糊了測試和解釋之間的區別。

迴歸測試最好限制在專用的物件或檔案中。組織測試有幾種選擇

  • 編寫包含互動式示例的測試用例文字檔案,並使用 testfile()DocFileSuite() 測試檔案。推薦這樣做,儘管對於從一開始就設計為使用 doctest 的新專案來說最容易。

  • 定義名為 _regrtest_topic 的函式,這些函式由單個文件字串組成,其中包含命名主題的測試用例。這些函式可以包含在與模組相同的檔案中,也可以分離到單獨的測試檔案中。

  • 定義一個 __test__ 字典,將回歸測試主題對映到包含測試用例的文件字串。

當您將測試放在模組中時,該模組本身可以成為測試執行器。當測試失敗時,您可以安排測試執行器在除錯問題時僅重新執行失敗的 doctest。這是一個此類測試執行器的最小示例

if __name__ == '__main__':
    import doctest
    flags = doctest.REPORT_NDIFF|doctest.FAIL_FAST
    if len(sys.argv) > 1:
        name = sys.argv[1]
        if name in globals():
            obj = globals()[name]
        else:
            obj = __test__[name]
        doctest.run_docstring_examples(obj, globals(), name=name,
                                       optionflags=flags)
    else:
        fail, total = doctest.testmod(optionflags=flags)
        print(f"{fail} failures out of {total} tests")

腳註