[Pythonのはじめ方] Part34: doctestの使い方

“`html

doctestとは?

doctestは、Pythonに標準で備わっているテストモジュールの一つです。このモジュールの最大の特徴は、関数やクラスのdocstring(ドキュメンテーション文字列)に書かれたインタラクティブなPythonセッションのようなコード例を、そのままテストケースとして実行できる点です。

これにより、コードのドキュメントを書きながら、そのドキュメントが示す通りの動作をするかを簡単にテストできます。まさに「実行可能なドキュメント」と言えるでしょう!

主なメリットは以下の通りです:

  • 簡単なテスト作成: docstringにコード例を書くだけでテストが作成できます。
  • ドキュメントとコードの一貫性: ドキュメント内のコード例が常に動作することを保証しやすくなります。
  • 手軽な導入: Python標準ライブラリなので、追加のインストールは不要です。

基本的な使い方

doctestの使い方は非常にシンプルです。

1. Docstringにテストを書く

テストしたい関数やクラスのdocstring内に、Pythonの対話モード (REPL) のような形式でコード例とその期待される出力を記述します。>>> が入力プロンプト、その次の行が期待される出力です。

def add(a, b):
    """
    二つの数値を加算します。

    >>> add(1, 2)
    3
    >>> add(-1, 1)
    0
    >>> add(5, 7)
    12
    """
    return a + b

def factorial(n):
    """
    非負整数nの階乗を計算します。

    >>> factorial(0)
    1
    >>> factorial(5)
    120
    >>> factorial(-1)
    Traceback (most recent call last):
        ...
    ValueError: n must be >= 0
    """
    if not isinstance(n, int):
        raise TypeError("n must be an integer")
    if n < 0:
        raise ValueError("n must be >= 0")
    if n == 0:
        return 1
    else:
        res = 1
        for i in range(1, n + 1):
            res *= i
        return res

例外が発生する場合もテストできます。期待される例外のトレースバックは、最後の行だけ(または重要な部分だけ)を記述し、それ以外の部分は ... で省略できます(後述のELLIPSISオプションも参照)。

2. テストを実行する

テストを実行するには、主に2つの方法があります。

方法1: スクリプト内で実行

テストを含むPythonスクリプトの最後に以下のコードを追加します。

if __name__ == "__main__":
    import doctest
    doctest.testmod(verbose=True) # verbose=True で詳細な結果を表示

doctest.testmod() 関数が、現在のモジュール内のdocstringを探してテストを実行します。verbose=True を指定すると、成功したテストも含めて詳細なレポートが出力されます。

方法2: コマンドラインから実行

ターミナルからPythonコマンドを使って直接テストを実行することもできます。

python -m doctest -v your_module.py

-m doctest でdoctestモジュールを実行し、-v オプションで詳細な結果を表示します。

実行結果の見方

すべてのテストが成功した場合、(verbose=True または -v なしの場合) 通常は何も表示されません。成功したテストの詳細を表示した場合や、失敗したテストがある場合は、以下のような形式で結果が表示されます。

Trying:
    add(1, 2)
Expecting:
    3
ok
Trying:
    add(-1, 1)
Expecting:
    0
ok
Trying:
    add(5, 7)
Expecting:
    12
ok
... (他のテスト) ...
1 items passed all tests:
   3 tests in __main__.add
   3 tests in __main__.factorial
6 tests in 2 items.
6 passed and 0 failed.
Test passed.

もしテストが失敗すると、期待した出力 (Expected) と実際の出力 (Got) が表示され、問題箇所を特定するのに役立ちます。

**********************************************************************
File "your_module.py", line X, in __main__.add
Failed example:
    add(5, 7)
Expected:
    11  # 間違った期待値
Got:
    12
**********************************************************************
... (他のテスト結果) ...

doctestのオプションと注意点

doctestには、テストの挙動を細かく制御するためのオプションフラグがあります。これらは doctest.testmod()optionflags 引数や、docstring内のディレクティブコメント (# doctest: +FLAG) で指定できます。

オプションフラグ ディレクティブ 説明
doctest.NORMALIZE_WHITESPACE +NORMALIZE_WHITESPACE 空白(スペースやタブ、改行)の数の違いを無視します。出力の見た目だけが異なる場合に便利です。
doctest.ELLIPSIS +ELLIPSIS 期待される出力中の ... が、実際の出力中の任意の部分文字列(空文字列を含む)にマッチするようにします。可変な部分(例:オブジェクトID)を無視するのに役立ちます。
doctest.IGNORE_EXCEPTION_DETAIL +IGNORE_EXCEPTION_DETAIL 例外が発生した場合、例外の型のみをチェックし、詳細メッセージは無視します。
doctest.SKIP +SKIP そのテスト例をスキップします。一時的にテストを無効化したい場合などに使います。
doctest.FAIL_FAST (なし) 最初のテスト失敗が発生した時点で、テスト実行を中断します。コマンドラインでは -f オプションで指定できます。
pytest用: ALLOW_UNICODE +ALLOW_UNICODE (pytest実行時) Python 2/3互換のため、期待される出力のユニコードリテラルプレフィックス u'' を無視します。
pytest用: ALLOW_BYTES +ALLOW_BYTES (pytest実行時) 同様に、バイトリテラルプレフィックス b'' を無視します。
pytest用: NUMBER +NUMBER (pytest実行時) 浮動小数点数を比較する際に、期待される出力に書かれた精度まで一致すればパスとします。

注意点:

  • 出力の一致: doctestは基本的に文字列として出力が完全に一致するかを比較します。空白や改行の違いも失敗の原因になるため、NORMALIZE_WHITESPACE が役立ちます。
  • 順序不定なコレクション: 辞書 (dict) やセット (set) の要素の順序は保証されません。Python 3.7以降では辞書の順序は挿入順になりましたが、それ以前のバージョンやセットでは注意が必要です。このような場合、出力をソートしたり、ELLIPSIS を使ったりする工夫が必要になることがあります。あるいは、doctest2 のような拡張ライブラリを検討する手もあります。
  • 副作用: doctest内のコードは実際に実行されるため、副作用(ファイル書き込みなど)がある場合は注意が必要です。テストの実行順序に依存するテストも避けるべきです。
  • 複雑なテスト: 複雑なセットアップやクリーンアップが必要なテスト、多くの条件分岐を網羅するテストには、unittestpytest のような本格的なテストフレームワークの方が適している場合があります。

メリットとデメリット

メリット デメリット
ドキュメントとテストが一体化しているため、使い方の例がそのままテストになる。 複雑なテストロジックやセットアップ/クリーンアップ処理には不向き。
コード例が常に最新の動作を反映するように保ちやすい。 出力の完全一致が求められるため、些細な変更(空白など)でテストが失敗しやすいことがある。(オプションで緩和可能)
Python標準ライブラリのため、追加インストール不要で手軽に始められる。 テストケースが増えるとdocstringが長くなり、可読性が低下する可能性がある。
シンプルな関数や基本的な動作の確認には非常に有効。 テストの発見や管理機能は、本格的なテストフレームワークに比べて限定的。

pytestとの連携

人気のテストフレームワークである pytest は、doctestを簡単に実行する機能を持っています。特別な設定なしに、pytest はモジュール内のdocstringや指定されたテキストファイル中のdoctestを発見し、実行してくれます。

pytest でdoctestを実行するには、コマンドラインで --doctest-modules オプションをつけます。

pytest --doctest-modules your_module.py

テキストファイル (例: `tests.txt`) 中のdoctestを実行する場合は、ファイル名を指定します。

pytest tests.txt

pytest.inipyproject.toml 設定ファイルに以下のように記述することで、毎回オプションを指定する手間を省けます。

[pytest]
addopts = --doctest-modules
doctest_optionflags = NORMALIZE_WHITESPACE ELLIPSIS IGNORE_EXCEPTION_DETAIL

pytest を使うと、通常の unittest/pytest スタイルのテストとdoctestを組み合わせて実行・管理できるため、非常に便利です。pytestの豊富な機能(フィクスチャ、マーキングなど)とdoctestの手軽さを両立できます。

まとめ

doctest は、Pythonコードのドキュメントと簡単なテストを同時に作成するための優れたツールです。特に、関数の基本的な使い方を示す例や、簡単な入出力の検証に適しています。

複雑なテストシナリオには unittestpytest が適していますが、doctestはこれらのフレームワークと組み合わせて使うことも可能です。まずは簡単な関数からdoctestを導入し、ドキュメントの品質向上と基本的な動作保証に役立ててみましょう!

参考情報

“`

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です