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
$

沒有輸出!這是正常的,這意味著所有示例都有效。將 -v 傳遞給指令碼,doctest 會列印它正在嘗試的詳細日誌,並在最後列印摘要:

$ 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 中找到。

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

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

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

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

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

python M.py

除非示例失敗,否則這不會顯示任何內容,在這種情況下,失敗的示例和失敗的原因將列印到 stdout,並且輸出的最後一行是 ***Test Failed*** N failures.,其中 *N* 是失敗的示例數量。

改為使用 -v 開關執行它:

python M.py -v

所有嘗試的示例的詳細報告將列印到標準輸出,以及最後各種摘要。

您可以透過將 verbose=True 傳遞給 testmod() 來強制使用詳細模式,或透過傳遞 verbose=False 來禁止它。在任何一種情況下,testmod() 都不會檢查 sys.argv(因此傳遞 -v 或不傳遞都沒有效果)。

還有一個用於執行 testmod() 的命令列快捷方式。您可以指示 Python 直譯器直接從標準庫執行 doctest 模組,並在命令列中傳遞模組名稱:

python -m doctest -v example.py

這將匯入 example.py 作為獨立模組,並在其上執行 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() 相同的格式列印到 stdout。

預設情況下,testfile() 在呼叫模組的目錄中查詢檔案。有關可用於指示其在其他位置查詢檔案的可選引數的說明,請參閱 基本 API 部分。

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

還有一個用於執行 testfile() 的命令列快捷方式。您可以指示 Python 直譯器直接從標準庫執行 doctest 模組,並在命令列中傳遞檔名:

python -m doctest -v example.txt

由於檔名不是以 .py 結尾,doctest 會推斷它必須使用 testfile() 執行,而不是 testmod()

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

工作原理

本節詳細探討 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 並不是試圖精確模擬任何特定的 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 類來使用不同的演算法來處理製表符。

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

  • 如果您在互動式會話中透過反斜槓繼續一行,或者出於任何其他原因使用反斜槓,則應使用原始文件字串,這將完全保留您鍵入的反斜槓

    >>> 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 中頂層定義的任何名稱,以及在正在執行的文件字串中較早定義的名稱。示例看不到在其他文件字串中定義的名稱。

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

異常怎麼辦?

沒問題,前提是回溯是示例產生的唯一輸出:只需貼上回溯即可。[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 選項。該示例中的省略號可以省略,或者也可以是三個(或三百個)逗號或數字,或者是蒙提·派森短劇的縮排筆錄。

您應該閱讀一次的一些詳細資訊,但不必記住

  • Doctest 無法猜測您的預期輸出是來自異常回溯還是來自普通列印。因此,例如,期望 ValueError: 42 is prime 的示例,無論是否實際引發 ValueError,或者如果該示例僅列印該回溯文字,都將透過。在實踐中,普通輸出很少以回溯標題行開頭,因此這不會造成實際問題。

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

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

  • 互動式 shell 會省略某些 SyntaxError 的回溯標題行。但是 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 命令列介面。

3.4 版本新增: -o 命令列選項。

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

doctest.DONT_ACCEPT_TRUE_FOR_1

預設情況下,如果預期輸出塊僅包含 1,則包含 1True 的實際輸出塊將被視為匹配,對於 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: 3*14,則期望 ValueError: 42 的示例將透過,但如果引發例如 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

指定後,涉及多行預期輸出和實際輸出的失敗將使用統一差異顯示。

doctest.REPORT_CDIFF

指定後,涉及多行預期輸出和實際輸出的失敗將使用上下文差異顯示。

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 命令列接受選項 -f 作為 -o FAIL_FAST 的簡寫。

3.4 版本新增。

doctest.REPORTING_FLAGS

一個按位或運算,將以上所有報告標誌組合在一起。

還有一種註冊新選項標誌名稱的方法,但這在你打算透過子類化擴充套件 doctest 內部結構時才有用

doctest.register_optionflag(name)

使用給定的名稱建立一個新的選項標誌,並返回新標誌的整數值。當子類化 OutputCheckerDocTestRunner 以建立你的子類支援的新選項時,可以使用 register_optionflag()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 庫在這方面的質量差異很大。

>>> 1./7  # risky
0.14285714285714285
>>> print(1./7) # safer
0.142857142857
>>> print(round(1./7, 6)) # much safer
0.142857

形式為 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* 字典來重用它以測試任意數量的子類。

如果為 true,則可選引數 *verbose* 會列印大量內容,如果為 false,則僅列印失敗;預設情況下,或如果為 None,則當且僅當 '-v'sys.argv 中時,它才為 true。

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

可選引數 *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* (或如果未提供 *m* 或為 None,則為模組 __main__) 可訪問的函式和類中的文件字串中的示例,從 m.__doc__ 開始。

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

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

返回 (failure_count, test_count)

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

可選引數 exclude_empty 預設為 false。如果為 true,則會排除未找到 doctest 的物件。預設值是為了向後相容,以便仍然使用 doctest.master.summarize 結合 testmod() 的程式碼仍然可以為沒有測試的物件獲取輸出。較新的 DocTestFinder 建構函式的 exclude_empty 引數預設為 true。

可選引數 extraglobsverbosereportoptionflagsraise_on_errorglobs 與上面的函式 testfile() 相同,只是 globs 預設為 m.__dict__

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

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

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

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

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

可選引數 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_relativeTrue(預設值),則 paths 中的每個檔名都指定一個與作業系統無關的模組相對路徑。預設情況下,此路徑相對於呼叫模組的目錄;但是,如果指定了 package 引數,則它相對於該包。為了確保與作業系統無關,每個檔名應使用 / 字元分隔路徑段,並且不能是絕對路徑(即,它不能以 / 開頭)。

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

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

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

可選引數 tearDown 指定測試套件的拆卸函式。在執行每個檔案中的測試之後呼叫此函式。tearDown 函式將傳遞一個 DocTest 物件。setUp 函式可以訪問作為傳遞的測試的 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 失敗,則合成的單元測試將失敗,並引發一個 failureException 異常,顯示包含測試的檔名和(有時是近似的)行號。如果 docstring 中的所有示例都被跳過,則合成的單元測試也會被標記為已跳過。

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

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

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

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

可選引數 setUptearDownoptionflags 與上面的 DocFileSuite() 函式相同。

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

在 3.5 版本中更改: 如果 module 不包含 docstring,DocTestSuite() 將返回一個空的 unittest.TestSuite,而不是引發 ValueError

exception doctest.failureException

當透過 DocFileSuite()DocTestSuite() 轉換為單元測試的 doctest 失敗時,將引發此異常,顯示包含測試的檔名和(有時是近似的)行號。

在底層,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 語句,與其預期輸出配對。

  • DocTest: 一個 Example 的集合,通常從單個 docstring 或文字檔案中提取。

定義了其他處理類來查詢、解析、執行和檢查 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_msgNone,否則它以換行符結尾。建構函式會在需要時新增換行符。

lineno

包含此示例的字串中示例開始處的行號。此行號相對於包含字串的開頭從零開始。

indent

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

options

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

DocTestFinder 物件

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

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

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

可選引數 *parser* 指定用於從文件字串中提取 doctest 的 DocTestParser 物件(或直接替換)。

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

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

DocTestFinder 定義以下方法

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

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

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

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

  • 作為預設名稱空間(如果未指定 *globs*)。

  • 防止 DocTestFinder 從其他模組匯入的物件中提取 DocTest。(包含模組不是 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 的。可選引數 name 是一個標識此字串的名稱,僅用於錯誤訊息。

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

將給定的字串分成示例和中間文字,並將它們作為交替的 Example 和字串的列表返回。Example 的行號是基於 0 的。可選引數 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 指定應使用的 OutputChecker 物件(或直接替換)來比較 doctest 示例的預期輸出和實際輸出。

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

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

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

DocTestRunner 定義以下方法

report_start(out, test, example)

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

example 是即將處理的示例。test包含示例的測試。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 用例支援 debug() 方法,該方法由 unittest.TestCase 定義。

  • 您可以在 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 的值為 true,則指令碼檔案將直接執行,並且只有當指令碼透過引發未處理的異常而終止時,偵錯程式才會介入。如果確實發生了這種情況,則會透過 pdb.post_mortem() 呼叫事後除錯,並傳遞來自未處理異常的回溯物件。如果未指定 pmpm 為 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 異常,其中包含測試、示例和實際輸出。

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

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() 返回的那樣。

隨想錄

如簡介中所述,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")

腳註