Pythonの標準ライブラリであるdoctestは、ドキュメント文字列(docstring)に書いた対話形式の使用例をそのまま実行し、期待する結果と一致するかを自動で確認できます。
初心者のうちは「まず動く例」を書きたいものです。
その例をテストにも使えるのがdoctestの魅力です。
この記事では、書き方から実行方法、つまずきやすい点まで丁寧に解説します。
Pythonのdoctestとは
doctestの基本
doctestは、関数やモジュールのドキュメント文字列に書いた>>>
から始まる対話形式の例を検出して実行し、その出力とdocstringに記した期待結果を文字列として比較します。
例えば、次のようなdocstringがあるとします。
- 入力例は
>>> expr
、改行して続ける場合は...
を使います。 - その直後に期待する出力をそのまま書きます。
doctestはPython標準ライブラリなので、追加のインストールは不要です。
メリット: サンプルとテストを一体化
サンプルコードがそのままテストになるため、ドキュメントと実装のズレが起きにくくなります。
学習者やチームメンバーは、docstringの例を読み、その例が常に正しいことをテストで保証できます。
結果として、導入も更新も軽く、動く仕様書のように活用できます。
ユニットテストとの違い
doctestは「使い方の例を動かして保証する」ことが得意で、unittestやpytestは「網羅的で柔軟な検証」が得意です。
状況に応じて使い分けます。
観点 | doctest | unittest/pytest |
---|---|---|
目的 | 例を通じた正しさの保証とドキュメントの信頼性向上 | 網羅的で柔軟な検証、詳細なアサーション |
記述場所 | 関数・モジュール・クラスのdocstring | 専用のテストファイル |
学習コスト | 低い | 中〜高 |
表現力 | 文字列一致が基本(オプションで拡張) | 幅広いアサーションと豊富なプラグイン |
実行速度 | 非常に速い | プロジェクト規模に依存 |
適した題材 | 小さく確実な例、使用方法の提示 | 複雑な分岐、外部I/O、モック活用 |
まずはdoctestで「小さく正しい例」を育て、複雑な検証はユニットテストで補完するのがおすすめです。
書き方の基本
関数のドキュメント文字列に対話形式の例を書く
関数のdocstringに、インタラクティブシェルのような形式で使用例を書きます。
返り値を検証する例、例外を検証する例を混ぜられます。
# 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(表示用の文字列表現)になります。
たとえば返り値が文字列ならクォート付きで表示されます。
def shout(name):
"""挨拶を大文字で返す関数です。
>>> shout("Alice")
'HELLO, ALICE!'
"""
return f"HELLO, {name.upper()}!"
printの出力も検証できます。
返り値ではなく標準出力を確認したいときに便利です。
def hello(name):
"""printの出力も検証できます。
>>> hello("Bob")
Hello, Bob!
"""
print(f"Hello, {name}!")
小さく確実な例にする
1つの例では1つのことだけを示し、入力と期待出力を短く保つと読みやすく壊れにくいテストになります。
複雑な状態や長い出力は後述のテクニック(ELLIPSISやNORMALIZE_WHITESPACE)を活用しましょう。
モジュールやクラスのdocstringにも書ける
関数だけでなく、モジュールやクラスにも例を書けます。
# 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)を付けると詳細が表示されます。
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
が使えます。
# 空白差や長い出力の省略に寛容にする例
python -m doctest -v -o NORMALIZE_WHITESPACE -o ELLIPSIS example_long.py
コード内で実行
スクリプトの末尾でdoctest.testmod()
を呼ぶ方法もあります。
学習用スクリプトや小さなモジュールで便利です。
# 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を使うと、連続する空白の差を無視できます。
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で省略できます。
...
が任意の文字列にマッチします。
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で真偽を確認するのが安全です。
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
誤差を含む値をそのまま期待出力に書くのは避けるのがコツです。
ランダム値や現在時刻は固定化してテストする
乱数や現在時刻は毎回変わるため、そのままでは一致しません。
依存性注入(パラメータで注入)やシード固定で安定させます。
# 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
が使えます。
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 -v
やdoctest.testmod()
で実行します。
長い出力には+ELLIPSIS
、空白には+NORMALIZE_WHITESPACE
、不安定な値には丸め、シード固定、依存性注入を使うと、初心者でも扱いやすく、読みやすいテストになります。
最初は小さな例から始め、「正しい使い方の例」を積み重ねることで、コードの品質とドキュメントの信頼性を同時に高めていきましょう。