doctestによるPythonicコードの文書化とテスト
Daniel Hayes
Full-Stack Engineer · Leapcell

はじめに
ソフトウェア開発のペースの速い世界では、コード品質の確保と最新のドキュメントの維持は最優先事項です。多くの場合、これら2つの重要な側面は別々、時には競合する懸念事項として扱われます。開発者は、コードとは別に存在する広範なテストスイートを作成し、その後、絶え間ない同期を必要とするドキュメントを作成するかもしれません。この伝統的なアプローチは、コードベースが進化するにつれて、ドキュメントが古くなり、テストの効果が低下する可能性があります。
あなたのドキュメントがテストである世界を想像してみてください。ユーザーに示す例が自動的に正しさのために検証され、コードの文書化という行為が本質的にその信頼性を強化する世界です。これは単なる空想ではありません。Pythonの強力なdoctest
モジュールのおかげで、これは現実のものとなります。doctest
は、テストケースをドキュメント文字列にシームレスに統合することで、自己文書化および自己テストコードを作成するためのユニークで非常に効果的な方法を提供します。この記事では、doctest
を使用してPython開発ワークフローを向上させるための原則、実装、および実践的なメリットを掘り下げます。
ドキュメントとテストの相乗効果
doctest
の詳細に入る前に、その哲学を支えるいくつかのコアコンセプトを明確にしましょう。
-
ドキュメント文字列(Docstrings): Pythonでは、ドキュメント文字列はモジュール、関数、クラス、またはメソッド定義の最初のステートメントとして現れる文字列リテラルです。これはインラインドキュメントとして機能し、コードが何をするか、その引数、および何を返すかを説明します。これらは
__doc__
属性を介してアクセス可能であり、SphinxのようなツールがAPIドキュメントを生成するために広く使用されています。 -
テスト駆動開発(TDD): テストが実際のコードの前に書かれるソフトウェア開発プロセスです。このアプローチは、コードが指定された要件を満たすことを保証し、堅牢でモジュール化されたコンポーネントの設計に役立ちます。
-
単体テスト(Unit Testing): ソフトウェアアプリケーションの個々の単位またはコンポーネントを分離してテストする実践です。目標は、ソフトウェアの各単位が設計どおりに実行されることを検証することです。
doctest
は、関数のドキュメント文字列またはモジュールのドキュメント文字列内に、期待される出力とともに例の使用法を埋め込むことを可能にすることで、ドキュメント文字列と単体テストの間のギャップを埋めます。これらの例は実行可能なテストとして機能します。
doctestの仕組み
doctest
モジュールは、ドキュメント文字列内でインタラクティブなPythonセッションのように見えるテキストを検索することによって機能します。具体的には、>>>
(Pythonインタラクティブプロンプト)で始まる行を探し、その後の行をそのインタラクティブコマンドの期待される出力として扱います。
doctest
が実行されると、>>>
プロンプトの後のコードを実行し、実際の出力をドキュメント文字列で提供された期待される出力と比較します。それらが一致すればテストは合格し、そうでなければ失敗します。
簡単な例でこれを説明しましょう。
def add(a, b): """ 2つの数値を加算し、その合計を返します。 >>> add(2, 3) 5 >>> add(10, -5) 5 >>> add(0, 0) 0 """ return a + b
このadd
関数では、ドキュメント文字列に3つの異なるテストケースが含まれています。各ケースは、add
がどのように呼び出されるべきかと、その対応する戻り値がどうなるべきかを示しています。
これらのテストを実行するには、スクリプトの末尾に簡単なブロックを追加できます。
import doctest def add(a, b): """ 2つの数値を加算し、その合計を返します。 >>> add(2, 3) 5 >>> add(10, -5) 5 >>> add(0, 0) 0 """ return a + b if __name__ == "__main__": doctest.testmod()
このスクリプトを実行すると:python your_module.py
、doctest.testmod()
は現在のモジュール内のすべてのdoctestを自動的に検出し、実行します。すべてのテストが合格した場合、出力は次のようになります。
$ python your_module.py
(デフォルトでは出力がないことは成功を意味します)
テストが失敗した場合、doctest
は不一致を報告します。
ここでは意図的に1つ壊してみましょう。
def add(a, b): """ 2つの数値を加算し、その合計を返します。 >>> add(2, 3) 6 """ return a + b if __name__ == "__main__": doctest.testmod()
これを実行すると、次のようになります。
$ python your_module.py
**********************************************************************
File "your_module.py", line 5, in __main__.add
Failed example:
add(2, 3)
Expected:
6
Got:
5
**********************************************************************
1 items had failures:
1 of 1 in __main__.add
***Test Failed*** 1 failures.
高度な機能とディレクティブ
doctest
は、単なる基本的なプロンプトと出力のマッチング以上のものを提供します。例外、順序のない出力、または浮動小数点数の比較を処理する状況には、特別なディレクティブを使用できます。
-
ELLIPSIS
: 出力の一部が変動する可能性がある場合(例:メモリ アドレスまたはオブジェクト ID)に便利です。任意のサブ文字列に一致させるには、期待される出力で...
を使用します。def get_object_info(obj): """ オブジェクトの型とIDの文字列表現を返します。 >>> get_object_info(1) "<class 'int'> at 0x...>" """ return f"{type(obj)} at {hex(id(obj))}"
これを有効にするには、
doctest.testmod()
にoptionflags=doctest.ELLIPSIS
を渡す必要があります。 -
期待される例外: テストケースが例外を発生させることが期待される場合、それを直接指定できます。
def divide(a, b): """ 2つの数値を割ります。 >>> divide(10, 2) 5.0 >>> divide(10, 0) Traceback (most recent call last): ... ZeroDivisionError: division by zero """ return a / b
-
NORMALIZE_WHITESPACE
: 期待される出力と実際の出力の比較時に、空白(スペース、タブ、改行)の違いを無視します。def format_list(items): """ アイテムのリストを文字列にフォーマットします。 >>> format_list(['apple', 'banana', 'cherry']) # doctest: +NORMALIZE_WHITESPACE "Items: - apple - banana - cherry" """ return "Items:\n" + "\n".join(f"- {item}" for item in items)
これらのフラグは、testmod()
全体でグローバルに有効にするか、doctest: +FLAG
を使用してローカルに有効にすることができます。
応用シナリオ
doctest
はいくつかのシナリオで輝きます。
- ドキュメント内の例: 主なユースケースです。ドキュメント文字列に示されたすべての使用例が常に正しく最新であることを保証します。これにより、ドキュメントへの信頼が構築されます。
- 簡単な関数テスト: 小さく、明確に定義された関数の場合、
doctest
は、別のunittest
またはpytest
スイートをセットアップすることなく、それらの期待される動作をキャプチャするための迅速かつ効果的な方法になる可能性があります。 - 教育資料: チュートリアルを作成したりPythonを教えたりする際に、
doctest
は学生に提供されるコードスニペットが実行可能で、宣伝された結果を生成することを確認します。 - 簡単な回帰チェック: バグが修正された場合、関連する関数のドキュメント文字列に新しい
doctest
を追加することで、バグが再発するのを防ぐことができ、シンプルながら効果的な回帰テストとして機能します。
ベストプラクティスと制限事項
強力ではありますが、doctest
はunittest
やpytest
のような包括的な単体テストフレームワークの代替となるものではありません。
doctest
を使用する場合:
- シンプルで説明的な例の場合。
- 関数、メソッド、クラスのパブリックAPIをテストする場合。
- テストケースが実行可能な例に自然に収まる場合。
他のフレームワークを使用する場合と制限事項:
- 複雑なセットアップ:
doctest
は、広範なセットアップ(例:データベース、外部サービス、または複雑なオブジェクトグラフのモック)を必要とするテストには理想的ではありません。 - フィクスチャ管理: 他のフレームワークは、テスト環境の初期化のための堅牢なフィクスチャ管理を提供します。
- パラメータ化されたテスト:
pytest
のパラメータ化を使用すると、同じテストロジックを異なる入力で実行することが容易になります。 - パフォーマンス テスト:
doctest
はパフォーマンスまたはストレステストのために設計されていません。 - エラー レポート:
doctest
は失敗を報告しますが、大規模なプロジェクトではpytest
やunittest
の詳細なレポート機能の方が優れていることがよくあります。
一般的なアプローチは、明確で公開facingの例にはdoctest
を使用し、内部的、複雑、またはエッジケースのテストにはpytest
またはunittest
を使用することです。
結論
doctest
は、「バッテリー付属」というPythonic哲学を体現しており、ドキュメントとテストを融合させるための軽量で効果的な方法を提供します。ドキュメント文字列に実行可能な例を直接埋め込むことで、本質的に自己検証可能で理解しやすいコードを作成します。このユニークなアプローチは、回帰を早期にキャッチすることでコード品質を向上させるだけでなく、ドキュメントがコードの実際の動作を常に反映するようにすることで保守性を劇的に向上させます。doctest
を採用し、あなたのドキュメントがコードの正確さの守護者となるようにしましょう。