Pythonのdoctest完全ガイド:ドキュメントとテストを融合する魔法

docstring を実行可能なテストに変える `doctest` ライブラリを徹底解説!

はじめに: doctestとは?

Python には、コードの品質を保つための強力なテストツールが組み込まれています。その中でもユニークな存在が `doctest` モジュールです。`doctest` は、関数のドキュメンテーション文字列 (docstring) や外部テキストファイルに含まれる対話的な Python セッションの例を探し出し、それらが正確に動作するかを検証します。

このアプローチの最大の魅力は、ドキュメントが常に最新のコード動作を反映する点にあります。コード例が古くなったり、間違っていたりすると、テストが失敗するため、ドキュメントの信頼性が向上します。また、簡単な使い方を示す例題に対して、別途テストコードを書く手間を省くことができます。

このブログ記事では、`doctest` の基本的な使い方から、少し複雑なシナリオに対応するための応用テクニック、さらには他のテストフレームワークとの比較まで、`doctest` を深く理解し、効果的に活用するための情報を網羅的に解説します。さあ、`doctest` の世界を探検しましょう!

基本的な使い方: docstring にテストを書く

`doctest` の最も一般的な使い方は、関数の docstring 内に Python の対話型インタプリタ(>>> プロンプトで始まる行)のような形式でコード例とその期待される出力を記述することです。

例として、2つの数値を足し合わせる簡単な関数 `add` を考えてみましょう。

import doctest
def add(a, b): """ 2つの数値を加算して返す関数。 >>> add(1, 2) 3 >>> add(5, -3) 2 >>> add(0, 0) 0 >>> add(1.5, 2.5) 4.0 """ return a + b
if __name__ == '__main__': doctest.testmod() # 現在のモジュール内の doctest を実行 

このコードでは、`add` 関数の docstring 内に4つのテストケースが記述されています。

  • >>> add(1, 2): これは実行するコードです。
  • 3: これは `add(1, 2)` を実行した際に期待される出力です。

最後の `if __name__ == ‘__main__’:` ブロックにある `doctest.testmod()` は、このスクリプトが直接実行されたときに、モジュール内の docstring をスキャンし、発見したテストを実行するための標準的な方法です。

この Python ファイル (例えば `my_math.py` という名前で保存) をコマンドラインから実行してみましょう。

python my_math.py 

もし全てのテストが成功すれば、何も出力されません。これは成功を示しています 。

テストの実行状況を詳しく見たい場合は、`-v` オプションを使います。

python my_math.py -v 

これにより、以下のような詳細な出力が得られます。

Trying: add(1, 2)
Expecting: 3
ok
Trying: add(5, -3)
Expecting: 2
ok
Trying: add(0, 0)
Expecting: 0
ok
Trying: add(1.5, 2.5)
Expecting: 4.0
ok
1 items had no tests: my_math
1 items passed all tests: 4 tests in my_math.add
4 tests in 2 items.
4 passed and 0 failed.
Test passed. 

もし期待される出力と実際の出力が異なると、テストは失敗し、エラーメッセージが表示されます。例えば、`add(1, 2)` の期待される出力を `4` に書き換えて実行すると、失敗レポートが出力されます。

doctest の仕組み: テキストからテストへ

`doctest` はどのようにして docstring からテストを見つけ出し、実行しているのでしょうか?その内部メカニズムを見ていきましょう。

  1. docstring の探索: `doctest.testmod()` が呼び出されると、指定されたモジュール(デフォルトでは呼び出し元のモジュール)内の関数、クラス、およびモジュール自身の docstring を再帰的に探索します。
  2. テスト例の抽出: docstring 内から、Python の対話型インタプリタのプロンプト >>> で始まる行を探します。これはテストコードの開始を示します。
  3. コードの実行: >>> に続くコードを抽出し、Python インタプリタで実行します。複数行にわたるコードブロック(インデントされたコードや、継続行を示す ... プロンプトを持つ行)も認識されます。
  4. 出力のキャプチャ: 実行されたコードが標準出力 (stdout) に何かを出力した場合、その出力をキャプチャします。
  5. 期待される出力との比較: テストコードの次の行から、次の >>> プロンプトまたは docstring の終わりまでを「期待される出力」として抽出します。キャプチャした実際の出力と、期待される出力を厳密に比較します。空白文字(スペース、タブ、改行)の違いも区別されます。
  6. 結果の報告: 全てのテスト例について比較を行い、一致しなかった場合は失敗として報告します。

この「厳密な比較」が `doctest` の特徴であり、時には扱いにくい点でもあります。例えば、出力の末尾に余計な空白があるだけでテストは失敗します。この厳密さを緩和するためのオプションも用意されています(後述)。

doctest の実行方法いろいろ

`doctest` を実行するには、いくつかの方法があります。

1. スクリプト内からの実行

既に見たように、テスト対象のコードが含まれるスクリプトの最後に `doctest.testmod()` を追加するのが最も一般的です。

import doctest
# ... (関数やクラスの定義) ...
if __name__ == '__main__': results = doctest.testmod() # オプション: 結果を表示したり、終了コードを設定したりできる if results.failed == 0: print(f"All {results.attempted} tests passed! ") else: print(f"{results.failed} tests failed out of {results.attempted}.") # exit(1) # CI 環境などでは失敗時に非ゼロ終了コードを返すことが多い 

`testmod()` は `TestResults` オブジェクト(試行数 `attempted` と失敗数 `failed` を持つ名前付きタプル)を返します。これを使って、テスト結果に応じた処理を追加できます。

2. コマンドラインからの実行

テスト対象のファイルに `testmod()` の呼び出しを追加したくない場合や、一時的にテストを実行したい場合は、コマンドラインから `doctest` モジュールを実行できます。

python -m doctest your_module.py 

詳細な出力を得るには `-v` オプションを追加します。

python -m doctest -v your_module.py 

この方法は、ライブラリとして提供されているモジュールのテストを、そのコードを変更することなく実行する場合に便利です。

3. 外部テキストファイルでのテスト

docstring だけではなく、独立したテキストファイルに `doctest` 形式のテストを記述することも可能です。これは、チュートリアルや README ファイル内のコード例が正しいことを保証するのに役立ちます。

例えば、`tutorial.txt` というファイルに以下のように記述します。

これはチュートリアルです。Python のリスト操作を見てみましょう。
リストの作成:
>>> my_list = [1, 2, 3]
>>> print(my_list)
[1, 2, 3]
要素の追加:
>>> my_list.append(4)
>>> my_list
[1, 2, 3, 4]
要素へのアクセス:
>>> my_list[0]
1
>>> my_list[-1]
4 

このファイルに対して `doctest` を実行するには、`doctest.testfile()` 関数を使います。

# test_runner.py
import doctest
if __name__ == '__main__': results = doctest.testfile("tutorial.txt", verbose=True) # verbose=True で詳細出力を有効に 

または、コマンドラインからも実行できます。

python -m doctest -v tutorial.txt 

これにより、ドキュメント内のコード例が常に動作することを保証できます。

doctest の書き方の注意点とTips

`doctest` はシンプルですが、その厳密さゆえにいくつか注意すべき点があります。また、これらをうまく扱うためのTipsも存在します。

1. 出力の一致 (空白と改行)

前述の通り、`doctest` は期待される出力と実際の出力を文字単位で比較します。

def greet(name): """ 挨拶を返す。 悪い例 (末尾に余計なスペース): >>> greet("World") 'Hello, World! ' 良い例: >>> greet("World") 'Hello, World!' """ return f"Hello, {name}!" 

上記の悪い例では、期待される出力 `’Hello, World! ‘` の末尾にスペースがあるため、テストは失敗します。

複数行の出力も同様です。改行の位置や数が異なると失敗します。

def print_lines(): """ 複数行を出力する。 >>> print_lines() Line 1 Line 2 Line 3 """ print("Line 1") print("Line 2") print() # 空行 print("Line 3") 

期待される出力の空行も正確に再現する必要があります。空白や改行の違いを無視したい場合は、後述の `NORMALIZE_WHITESPACE` オプションが役立ちます。

2. 例外のテスト

関数が特定の条件下で例外を送出することをテストしたい場合、`doctest` はそのトレースバック(エラーメッセージ)をキャプチャして比較します。

def divide(a, b): """ 割り算を行う。ゼロ除算の場合は ZeroDivisionError を送出する。 >>> divide(10, 2) 5.0 >>> divide(5, 0) Traceback (most recent call last): ... ZeroDivisionError: division by zero """ if b == 0: raise ZeroDivisionError("division by zero") return a / b 

期待される出力として `Traceback (most recent call last):` で始まるトレースバック情報を記述します。トレースバックの内容は環境によって微妙に異なる可能性があるため、可変な部分(ファイルパスや行番号など)は `…` (後述の `ELLIPSIS` オプションが暗黙的に有効になる)で置き換えるのが一般的です。重要なのは、最後の行の例外タイプとメッセージが一致することです。

例外メッセージの詳細部分を無視したい場合は、`IGNORE_EXCEPTION_DETAIL` オプションが利用できます(ただし、Python 3.2 以降で非推奨、代わりに `ELLIPSIS` の使用が推奨されています)。

3. 順序が保証されないデータ型 (辞書、集合)

辞書 (dict) や集合 (set) の要素の順序は、Python のバージョンや実行タイミングによって変わる可能性があります。そのまま出力すると、テストが不安定になることがあります。

def get_info(): """ ユーザー情報を辞書で返す。(順序は不定) 順序に依存するため、このテストは不安定になる可能性がある: >>> get_info() # doctest: +SKIP {'name': 'Alice', 'age': 30, 'city': 'Tokyo'} 順序を気にしないテスト (例: キーでソートしてから比較): >>> info = get_info() >>> sorted(info.items()) [('age', 30), ('city', 'Tokyo'), ('name', 'Alice')] """ # 実際にはDBアクセスなどがあるかもしれない return {'name': 'Alice', 'age': 30, 'city': 'Tokyo'}
def get_unique_items(items): """ リストからユニークな要素を集合で返す。(順序は不定) 順序を気にしないテスト (例: sorted でリストに変換して比較): >>> unique = get_unique_items([1, 3, 2, 1, 3]) >>> sorted(list(unique)) [1, 2, 3] """ return set(items) 

このような場合、直接比較する代わりに、`sorted()` などを使って順序を固定してから比較するのが良い方法です。上記の例では、辞書の `items()` をソートしたり、集合をリストに変換してソートしたりしています。

4. ワイルドカード (`ELLIPSIS`)

出力の一部が可変である場合や、重要でない部分を無視したい場合に `…` (Ellipsis) を使用できます。これを使うには、`# doctest: +ELLIPSIS` ディレクティブをテスト例に追加するか、`doctest.testmod(optionflags=doctest.ELLIPSIS)` のようにオプションで指定します。

`ELLIPSIS` は、期待される出力中の `…` が、実際の出力中の任意の部分文字列(空文字列を含む)にマッチすることを許可します。

import time
def process_data(data): """ データを処理し、処理時間を含むメッセージを返す。 処理時間は実行ごとに変わるため ELLIPSIS を使う。 >>> process_data("Sample") # doctest: +ELLIPSIS Processing Sample... Done (took ... seconds) """ start = time.time() # ... 何らかの処理 ... time.sleep(0.1) # 例として少し待機 end = time.time() return f"Processing {data}... Done (took {end - start:.3f} seconds)"
class MyObject: """ オブジェクトの文字列表現。アドレス部分は ELLIPSIS で無視。 >>> obj = MyObject() >>> print(obj) # doctest: +ELLIPSIS <MyObject object at 0x...> """ def __repr__(self): return f"<MyObject object at {hex(id(self))}>" 

例外のトレースバックをテストする際にも `…` が内部的に使われています。

5. ディレクティブ (`# doctest: +OPTION`)

特定のテスト例に対して `doctest` の動作を変更したい場合、テストコードの行末に `# doctest: +OPTION` または `# doctest: -OPTION` という形式でディレクティブを追加します。`+` はオプションの有効化、`-` は無効化(主にグローバル設定を上書きする場合)を意味します。

複数のオプションはカンマ区切りで指定できます: `# doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE`

よく使われるオプションを表にまとめます。

オプション名定数 (doctest モジュール内)説明
ELLIPSISdoctest.ELLIPSIS期待される出力中の ... をワイルドカードとして扱います。可変な部分(メモリアドレス、時間、トレースバックの一部など)にマッチさせます。
NORMALIZE_WHITESPACEdoctest.NORMALIZE_WHITESPACE空白(スペース、タブ、改行)の連続を単一のスペースとして扱い、行頭・行末の空白を無視します。これにより、出力フォーマットの微妙な違いによるテスト失敗を防ぎやすくなります。
IGNORE_EXCEPTION_DETAILdoctest.IGNORE_EXCEPTION_DETAIL(Python 3.2以降非推奨) 例外が発生した場合、例外の型のみを比較し、メッセージの詳細部分は無視します。ELLIPSIS を使ってメッセージの可変部分を無視する方が推奨されます。
SKIPdoctest.SKIPこのディレクティブが付いたテスト例をスキップします。一時的にテストを無効化したい場合などに使用します。
ALLOW_UNICODEdoctest.ALLOW_UNICODE(Python 3 では通常不要) Unicodeリテラル (u'...') の出力と通常の文字列リテラル ('...') の出力を区別しません。主に Python 2 との互換性のために存在します。
ALLOW_BYTESdoctest.ALLOW_BYTESバイト列リテラル (b'...') の出力と通常の文字列リテラル ('...') の出力を区別しません。デフォルトでは区別されます。
NUMBERdoctest.NUMBER(Python 3.12以降) 浮動小数点数の表現の違いを無視します。例えば、1.01.001e-60.000001 が同じとみなされます。

これらのオプションは `doctest.testmod(optionflags=…)` や `doctest.testfile(optionflags=…)` の引数として指定することで、モジュール全体やファイル全体に適用することも可能です。

import doctest
def format_data(data): """ データを整形して表示。空白の扱いに NORMALIZE_WHITESPACE を使う。 >>> format_data({' a ': 1, 'b': 2 }) # doctest: +NORMALIZE_WHITESPACE Key: a , Value: 1 Key: b , Value: 2 """ # 内部では空白の扱いが異なるかもしれない # 例: キー ' a ' の前後の空白を保持 for k, v in data.items(): print(f"Key:{k}, Value:{v}") # 出力例: Key: a , Value:1
# モジュール全体で ELLIPSIS と NORMALIZE_WHITESPACE を有効にする場合
# if __name__ == '__main__':
# doctest.testmod(optionflags=doctest.ELLIPSIS | doctest.NORMALIZE_WHITESPACE) 

注意: オプションはビットフラグとして定義されているため、複数指定する場合はビット単位OR演算子 | を使用します。

doctest の高度な使い方

基本的な使い方に加え、`doctest` にはより複雑な状況に対応するための機能も備わっています。

1. テストコンテキストの共有 (`globs` パラメータ)

`doctest` は通常、各 docstring やテキストファイルごとに独立した実行環境(名前空間)でテストを実行します。しかし、テスト間で共有したい変数やオブジェクトがある場合、`testmod()` や `testfile()` の `globs` パラメータを使用できます。

`globs` には、テスト実行時のグローバル名前空間として使用される辞書を指定します。

# common_setup.py
class DatabaseConnection: def query(self, sql): print(f"Executing: {sql}") if "users" in sql: return [{'id': 1, 'name': 'Alice'}, {'id': 2, 'name': 'Bob'}] return []
# グローバルな接続オブジェクト(例)
db_conn = DatabaseConnection()
def query_users(): """ 共有された 'db_conn' を使ってユーザーを検索する。 >>> users = query_users() Executing: SELECT * FROM users >>> len(users) 2 >>> users[0]['name'] 'Alice' """ # この関数内では 'db_conn' は未定義だが、 # テスト実行時に globs で渡されることを期待する return db_conn.query("SELECT * FROM users")
if __name__ == '__main__': import doctest # テスト実行時に db_conn を含むグローバル名前空間を渡す doctest.testmod(globs={'db_conn': db_conn}, verbose=True) 

この例では、`query_users` 関数自体は `db_conn` を直接インポートしていませんが、`testmod` の `globs` パラメータによってテスト実行環境に `db_conn` が注入され、docstring 内のテストコードから利用できるようになります。これは、テスト用の設定やモックオブジェクトを渡すのに便利です。

2. テスト対象の明示的な指定 (`DocTestFinder`)

`testmod()` はデフォルトでモジュール内の全ての docstring を探しますが、テスト対象をより細かく制御したい場合があります。`doctest.DocTestFinder` クラスを使うと、特定のオブジェクト(クラス、関数、モジュール)から docstring を見つけ出すプロセスをカスタマイズできます。

import doctest
class MyClass: """クラスの docstring""" def method1(self): """ メソッド1 の docstring >>> c = MyClass() >>> c.method1() 'method1 called' """ return 'method1 called' def method2(self): """メソッド2 の docstring (テストなし)""" return 'method2 called'
def standalone_function(): """ 独立した関数の docstring >>> standalone_function() 'standalone called' """ return 'standalone called'
if __name__ == '__main__': # MyClass のメソッドのみをテスト対象とする例 finder = doctest.DocTestFinder() # find メソッドでテスト対象オブジェクトを指定 # exclude_empty=False にするとテストがない docstring も見つける tests = finder.find(MyClass, "MyClass") # DocTestRunner を使って見つけたテストを実行 runner = doctest.DocTestRunner() for test in tests: runner.run(test) runner.summarize() # 結果の要約を表示 

`DocTestFinder` は、特定のオブジェクトやその属性を探索し、`DocTest` オブジェクトのリストを返します。その後、`DocTestRunner` を使ってこれらのテストを実行します。これにより、大規模なプロジェクトでテスト対象を絞り込むことが可能になります。

3. カスタム `DocTestRunner`

テストの実行方法や結果の報告方法をカスタマイズしたい場合は、`doctest.DocTestRunner` をサブクラス化して独自のランナーを作成できます。例えば、テストの失敗時に特定のアクションを実行したり、異なるフォーマットで結果を出力したりすることが考えられます。

import doctest
import sys
class CustomRunner(doctest.DocTestRunner): def report_failure(self, out, test, example, got): # デフォルトの失敗報告に加えて、追加情報を出力 out(f"--- CUSTOM FAILURE REPORT for {test.name} ---") super().report_failure(out, test, example, got) out(f"Example source:\n{example.source}") out(f"------------------------------------------") def summarize(self, verbose=None): # デフォルトの要約に加えて、カスタムメッセージを出力 super().summarize(verbose) if self.failures > 0: print(" Oh no! Some doctests failed. Please check the logs.", file=sys.stderr) else: print(" All doctests passed successfully!")
# 使い方 (例)
# runner = CustomRunner(verbose=True)
# finder = doctest.DocTestFinder()
# tests = finder.find(some_module)
# for test in tests:
# runner.run(test)
# runner.summarize() 

`report_start`, `report_success`, `report_failure`, `report_unexpected_exception`, `summarize` などのメソッドをオーバーライドすることで、テストライフサイクルの各段階での挙動を変更できます。

doctest の利点と欠点

`doctest` は便利なツールですが、万能ではありません。そのメリットとデメリットを理解し、適切な場面で活用することが重要です。

利点 (Pros)

  • ドキュメントとテストの一体化: これが最大の利点です。ドキュメント内のコード例が常に検証されるため、ドキュメントの正確性が保たれ、ユーザーは信頼できる例を参照できます。
  • シンプルさ: 簡単な関数やクラスの基本的な動作を示すテストを記述するのは非常に簡単です。特別なテスト構文を覚える必要はほとんどありません。
  • 学習コストが低い: Python の対話型セッションを知っていれば、すぐに `doctest` を書き始められます。
  • 読みやすさ: テストがドキュメントの一部として自然に読めるため、コードの意図や使い方が理解しやすくなります。
  • 回帰テスト: コードを変更した際に、意図しない動作変更(回帰)が発生していないかを簡単にチェックできます。

欠点 (Cons)

  • 複雑なテストには不向き: 多くのセットアップや後処理(ティアダウン)が必要なテスト、外部リソース(DB、ネットワーク)への依存が大きいテスト、状態を持つオブジェクトの複雑なインタラクションをテストするには `doctest` は適していません。
  • 出力の厳密さ: 空白や改行、浮動小数点数の微妙な表現の違いなどでテストが失敗しやすく、`NORMALIZE_WHITESPACE` や `ELLIPSIS` などのオプションが必要になる場面が多いです。
  • テストの分離が難しい: 全てのテストが同じ docstring 内にあるため、特定のテストだけを実行したり、グループ化したりするのが標準機能では難しいです。
  • リファクタリングへの影響: 関数名や引数を変更すると、それを使用している docstring 内のテストコードも全て修正する必要があります。また、docstring の記述スタイルによっては、コードのリファクタリング時にテストが壊れやすくなることがあります。
  • テストフィクスチャの欠如: `unittest` や `pytest` が提供するような、テスト間で共有されるセットアップ・ティアダウンの仕組み(フィクスチャ)が組み込みでは用意されていません (`globs` は限定的な代替手段です)。
  • テスト発見の仕組みが限定的: 基本的には docstring 内のテストしか発見しません。テストコードを別ファイルに体系的に整理したい場合には不向きです。

他のテストフレームワークとの比較: unittest と pytest

Python には `doctest` 以外にも、より高機能なテストフレームワークが存在します。代表的なものとして、標準ライブラリの `unittest` と、サードパーティ製の `pytest` があります。

特徴doctestunittestpytest
主な目的ドキュメント内のコード例の検証、シンプルな関数のテストユニットテスト、統合テストのための標準的なフレームワーク高機能で拡張性の高いテストフレームワーク、シンプルなテスト記述
テストの記述場所Docstring, テキストファイルテストクラス内のメソッド (test_ で始まる)テスト関数 (test_ で始まる)、テストクラス
セットアップ/ティアダウン限定的 (globs)setUp, tearDown, setUpClass, tearDownClass メソッド高機能なフィクスチャ (関数スコープ、クラススコープ、モジュールスコープ、セッションスコープ)
アサーション出力比較 (厳密)self.assertEqual(), self.assertTrue(), self.assertRaises() など多数標準の assert 文 (詳細な失敗情報付き)
テスト発見Docstring を探索TestLoader による規約ベース (test*.py ファイル、Test* クラス、test_* メソッド)規約ベース (test_*.py / *_test.py ファイル、Test* クラス、test_* 関数) + プラグインによる拡張
複雑さ低い中程度 (クラスベース)低い (シンプルなテスト)、高い (高度な機能)
拡張性低い中程度非常に高い (豊富なプラグインエコシステム)
使い分けのヒントAPIドキュメントの例、簡単なユーティリティ関数のテスト標準ライブラリのみで完結させたい場合、xUnit スタイルのテスト中規模以上のプロジェクト、簡潔なテスト、高度なフィクスチャ、プラグイン利用

doctest との併用

`doctest` は他のフレームワークと排他的な関係にあるわけではありません。実際、多くのプロジェクトでは `unittest` や `pytest` と `doctest` を併用しています。

  • doctest: 関数の基本的な使い方を示す簡単な例や、ドキュメント内のコード例の検証に使う。
  • unittest/pytest: より複雑なロジック、エッジケース、例外処理、外部システムとの連携などをテストするために使う。

`pytest` は、特別な設定なしに `doctest` を自動的に発見し、実行する機能を持っています (`–doctest-modules` オプション)。これにより、既存の `doctest` を活かしつつ、より複雑なテストは `pytest` の流儀で書く、というハイブリッドなアプローチが容易になります。

# pytest を実行すると、通常の pytest テストと doctest の両方が実行される
pytest --doctest-modules your_package/ 

このように、それぞれのツールの得意分野を理解し、組み合わせて使うことで、より堅牢で信頼性の高いコードベースを構築することができます 。

まとめ: doctest を使いこなそう!

`doctest` は、Python の docstring を生きたドキュメント兼テストケースに変えるユニークで強力なツールです。そのシンプルさから、特にライブラリの API ドキュメントやチュートリアルに含まれるコード例の正確性を保証したり、簡単なユーティリティ関数の基本的な動作を確認したりするのに非常に適しています。

一方で、出力の厳密さや、複雑なテストシナリオへの対応能力には限界があります。これらの点を理解し、`ELLIPSIS` や `NORMALIZE_WHITESPACE` などのオプション、あるいは `unittest` や `pytest` といった他のテストフレームワークとの適切な使い分け・併用が、`doctest` を効果的に活用する鍵となります。

ぜひ、あなたのプロジェクトでも `doctest` を導入し、ドキュメントの品質向上とテストの手間削減を体験してみてください。Happy testing!