閉じる

Pythonのdoctest入門: ドキュメント文字列をテストとして実行する方法

Pythonの標準ライブラリであるdoctestは、ドキュメント文字列(docstring)に書いた対話形式の使用例をそのまま実行し、期待する結果と一致するかを自動で確認できます。

初心者のうちは「まず動く例」を書きたいものです。

その例をテストにも使えるのがdoctestの魅力です。

この記事では、書き方から実行方法、つまずきやすい点まで丁寧に解説します。

Pythonのdoctestとは

doctestの基本

doctestは、関数やモジュールのドキュメント文字列に書いた>>>から始まる対話形式の例を検出して実行し、その出力とdocstringに記した期待結果を文字列として比較します。

例えば、次のようなdocstringがあるとします。

  • 入力例は>>> expr、改行して続ける場合は...を使います。
  • その直後に期待する出力をそのまま書きます。

doctestはPython標準ライブラリなので、追加のインストールは不要です。

メリット: サンプルとテストを一体化

サンプルコードがそのままテストになるため、ドキュメントと実装のズレが起きにくくなります。

学習者やチームメンバーは、docstringの例を読み、その例が常に正しいことをテストで保証できます。

結果として、導入も更新も軽く、動く仕様書のように活用できます。

ユニットテストとの違い

doctestは「使い方の例を動かして保証する」ことが得意で、unittestやpytestは「網羅的で柔軟な検証」が得意です。

状況に応じて使い分けます。

観点doctestunittest/pytest
目的例を通じた正しさの保証とドキュメントの信頼性向上網羅的で柔軟な検証、詳細なアサーション
記述場所関数・モジュール・クラスのdocstring専用のテストファイル
学習コスト低い中〜高
表現力文字列一致が基本(オプションで拡張)幅広いアサーションと豊富なプラグイン
実行速度非常に速いプロジェクト規模に依存
適した題材小さく確実な例、使用方法の提示複雑な分岐、外部I/O、モック活用
ポイント

まずはdoctestで「小さく正しい例」を育て、複雑な検証はユニットテストで補完するのがおすすめです。

書き方の基本

関数のドキュメント文字列に対話形式の例を書く

関数のdocstringに、インタラクティブシェルのような形式で使用例を書きます。

返り値を検証する例、例外を検証する例を混ぜられます。

Python
# example_basic.py

def add(x, y):
    """2つの値を足し算します。

    返り値を検証する基本的な例です。

    >>> add(1, 2)
    3
    >>> add(-1, 5)
    4
    >>> add("a", "b")
    'ab'

    例外を検証することもできます。以下は引数不足の例です。

    >>> add(1)  # 引数が足りないとTypeErrorになります
    Traceback (most recent call last):
        ...
    TypeError: add() missing 1 required positional argument: 'y'
    """
    return x + y

この書き方なら、使い方の説明とテストが1か所にまとまるので保守が楽です。

期待する出力をそのまま書く

doctestは文字列として完全一致を確認します。

返り値の表示はrepr(表示用の文字列表現)になります。

たとえば返り値が文字列ならクォート付きで表示されます。

Python
def shout(name):
    """挨拶を大文字で返す関数です。

    >>> shout("Alice")
    'HELLO, ALICE!'
    """
    return f"HELLO, {name.upper()}!"

printの出力も検証できます。

返り値ではなく標準出力を確認したいときに便利です。

Python
def hello(name):
    """printの出力も検証できます。

    >>> hello("Bob")
    Hello, Bob!
    """
    print(f"Hello, {name}!")

小さく確実な例にする

1つの例では1つのことだけを示し、入力と期待出力を短く保つと読みやすく壊れにくいテストになります。

複雑な状態や長い出力は後述のテクニック(ELLIPSISやNORMALIZE_WHITESPACE)を活用しましょう。

モジュールやクラスのdocstringにも書ける

関数だけでなく、モジュールやクラスにも例を書けます。

Python
# example_structured.py
"""モジュール全体の使い方の例です。

>>> add(2, 3)
5
"""

def add(a, b):
    """足し算を行う関数です。

    >>> add(10, 20)
    30
    """
    return a + b

class Accumulator:
    """合計値を貯める簡単なクラスです。

    >>> acc = Accumulator()
    >>> acc.add(5)
    >>> acc.total
    5
    """
    def __init__(self) -> None:
        self.total = 0

    def add(self, value: int) -> None:
        """値を加算します。

        >>> acc = Accumulator()
        >>> acc.add(3)
        >>> acc.total
        3
        """
        self.total += value

モジュールdocstringでは、そのモジュール内の関数を直接呼び出せます。

実行方法

コマンドラインで実行

ファイルに書いたdocstringのテストは、python -m doctestで実行できます。

-v(verbose)を付けると詳細が表示されます。

Shell
python -m doctest -v example_basic.py

実行すると、次のような出力が得られます。

Trying:
    add(1, 2)
Expecting:
    3
ok
Trying:
    add(-1, 5)
Expecting:
    4
ok
Trying:
    add("a", "b")
Expecting:
    'ab'
ok
Trying:
    add(1)
Expecting:
    Traceback (most recent call last):
        ...
    TypeError: add() missing 1 required positional argument: 'y'
ok
1 items passed all tests:
   4 tests in example_basic.add
4 tests in 1 items.
4 passed and 0 failed.
Test passed.

オプションフラグを全体に適用したい場合は-oが使えます。

Shell
# 空白差や長い出力の省略に寛容にする例
python -m doctest -v -o NORMALIZE_WHITESPACE -o ELLIPSIS example_long.py

コード内で実行

スクリプトの末尾でdoctest.testmod()を呼ぶ方法もあります。

学習用スクリプトや小さなモジュールで便利です。

Python
# example_run_in_code.py

def mul(a, b):
    """掛け算します。

    >>> mul(2, 5)
    10
    """
    return a * b

if __name__ == "__main__":
    # verbose=Trueで詳細表示、戻り値は失敗数と試行数
    import doctest
    result = doctest.testmod(verbose=True)
    print("summary:", result)

想定される出力は次の通りです。

実行結果
Trying:
    mul(2, 5)
Expecting:
    10
ok
1 items passed all tests:
   1 tests in example_run_in_code.mul
1 tests in 1 items.
1 passed and 0 failed.
Test passed.
summary: TestResults(failed=0, attempted=1)

成功/失敗メッセージの読み方

失敗すると、どのファイルの何行目のどの例が失敗したか、期待と実際の差が表示されます。

実行結果
**********************************************************************
File "example_basic.py", line 8, in example_basic.add
Failed example:
    add(1, 2)
Expected:
    4
Got:
    3
**********************************************************************
1 items had failures:
   1 of 4 in example_basic.add
***Test Failed*** 1 failures.

ExpectedとGotの差を見て、docstringの期待値か実装のどちらを直すか判断します。

よくあるつまずきとコツ

空白や改行の差で失敗する

doctestはデフォルトで空白や改行を厳密に比較します。

見た目は同じでも半角スペースの数が違うと失敗します。

対処としてNORMALIZE_WHITESPACEを使うと、連続する空白の差を無視できます。

Python
def columns(a, b, c):
    """列を整形して出力します。

    # 空白が複数あっても、1個の空白として比較します
    >>> print(columns("A", "BB", "CCC"))  # doctest: +NORMALIZE_WHITESPACE
    A    BB   CCC
    """
    # 幅を揃えるため空白が増える
    return f"{a:>1} {b:>4} {c:>3}"
ポイント

指示は>>>行の末尾に# doctest: +NORMALIZE_WHITESPACEのように書きます。

長い出力の一部だけ確認する

長いリストやJSONなどは、すべてを書かずELLIPSISで省略できます。

...が任意の文字列にマッチします。

Python
def make_long_list(n):
    """0からn-1までのリストを作ります。

    >>> items = make_long_list(100)
    >>> items[:10]  # 先頭だけ確認
    [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

    # 全体をざっくり確認したいときはELLIPSIS
    >>> items  # doctest: +ELLIPSIS
    [0, 1, 2, ..., 97, 98, 99]
    """
    return list(range(n))
注意

ELLIPSISを使うには# doctest: +ELLIPSISを付けます。

コマンドライン全体へは-o ELLIPSISで適用できます。

浮動小数点は丸めて比較する

浮動小数点は0.1 + 0.2のような演算で誤差が出ます。

そのまま比較すると意図せず失敗します。

丸めるか、iscloseで真偽を確認するのが安全です。

Python
import math

def ratio(a, b):
    """比率を返します。

    # 丸めて比較する例
    >>> round(ratio(1, 3), 4)
    0.3333

    # iscloseで真偽を確認する例
    >>> math.isclose(ratio(1, 3), 0.3333333, rel_tol=1e-6)
    True
    """
    return a / b

誤差を含む値をそのまま期待出力に書くのは避けるのがコツです。

ランダム値や現在時刻は固定化してテストする

乱数や現在時刻は毎回変わるため、そのままでは一致しません。

依存性注入(パラメータで注入)やシード固定で安定させます。

Python
# example_deterministic.py
import random
from datetime import datetime

def pick_index(rng=None):
    """0〜9の乱数インデックスを返します。

    # シード固定で値を安定化する
    >>> rng = random.Random(0)
    >>> pick_index(rng)
    6
    """
    rng = rng or random.Random()
    return rng.randrange(10)

def iso_now(now_fn=None):
    """現在時刻のISO文字列を返します。

    # 現在時刻を関数として注入して固定化する
    >>> fixed = lambda: datetime(2024, 1, 1, 12, 0, 0)
    >>> iso_now(fixed)
    '2024-01-01T12:00:00'
    """
    now_fn = now_fn or datetime.now
    return now_fn().isoformat()

乱数の具体的な値は処理内容やPythonの実装に依存する可能性があるため、必要に応じて+ELLIPSISと組み合わせるか、より直接的に丸めや比較の形を工夫してください。

例外の詳細を無視したいとき

エラーメッセージの細部が環境で揺れる場合はIGNORE_EXCEPTION_DETAILが使えます。

Python
def must_positive(x):
    """正の数でなければならない例。

    >>> must_positive(-1)  # doctest: +IGNORE_EXCEPTION_DETAIL
    Traceback (most recent call last):
        ...
    ValueError: ...
    """
    if x <= 0:
        raise ValueError(f"must be positive: {x}")
    return x

まとめ

doctestは「ドキュメントとして読めるサンプル」を「壊れないように守るテスト」に変える強力な標準機能です。

基本は>>>で入力、期待する出力をそのまま書き、python -m doctest -vdoctest.testmod()で実行します。

長い出力には+ELLIPSIS、空白には+NORMALIZE_WHITESPACE、不安定な値には丸め、シード固定、依存性注入を使うと、初心者でも扱いやすく、読みやすいテストになります。

最初は小さな例から始め、「正しい使い方の例」を積み重ねることで、コードの品質とドキュメントの信頼性を同時に高めていきましょう。

この記事を書いた人
エーテリア編集部
エーテリア編集部

人気のPythonを初めて学ぶ方向けに、文法の基本から小さな自動化まで、実際に手を動かして理解できる記事を書いています。

クラウドSSLサイトシールは安心の証です。

URLをコピーしました!