Python WebAcademy Blog

Pythonのdoctestとは?docstringに書いた使用例をそのままテストにする方法を初心者向けに解説

|

関数の説明に書いた使用例が、いつのまにか実際の動きとずれていた。そんな経験はありませんか。Python標準ライブラリのdoctestを使うと、docstringに書いた対話モード風の使用例を、そのままテストとして実行できます。基本の書き方、python -m doctestでの実行、失敗したときの読み方、例外やELLIPSISの扱い、pytestとの組み合わせまで、手元で動かした結果とともに解説します。

関数の説明に使用例を書いておいたのに、あとで中身を直したら、その例が嘘になっていた。そんな経験はありませんか。

説明文はプログラムと違って、間違っていてもエラーを出してくれません。だからこそ、気づかないうちに古くなってしまいます。

そこで役に立つのが、Pythonの標準ライブラリに入っているdoctestです。説明文に書いた使用例を、そのままテストとして実行してくれる仕組みです。

今回は、doctestの書き方と動かし方、失敗したときの読み方、少し困る場面の乗り越え方まで、順番に見ていきます。テストを書いたことがない方でも大丈夫です。

doctestとは何か

まずは、doctestがどんなものなのかをつかんでおきましょう。ひとことで言うと、説明文の中の使用例を実行して、書いてある結果と同じかを確かめる道具です。

Pythonを対話モードで動かすと、>>> のあとに入力したコードの結果が次の行に表示されますよね。doctestは、この見た目をそのまま説明文に書いておくと、実際に実行して答え合わせをしてくれます。

説明文というのは、関数の先頭に三重引用符で書くdocstringのことです。docstringの書き方そのものに自信がない方は、先にこちらを読んでおくと理解がスムーズです。【関連記事】Pythonのdocstringとは?3か月後の自分を助ける説明の書き方を初心者向けに解説

doctestは標準ライブラリなので、pipでインストールする必要はありません。Pythonが入っていれば、今日からすぐに使えます。

最初のdoctestを書いてみよう

では、実際に書いてみましょう。税込み価格を計算する小さな関数を例にします。

次のコードを、price.pyという名前で保存してください。docstringの中に、>>> で始まる行と、その結果を書いているのがポイントです。

def tax_included(price, rate=0.1):
    """税込み価格を返す。

    >>> tax_included(1000)
    1100
    >>> tax_included(980, rate=0.08)
    1058
    >>> tax_included(-1)
    Traceback (most recent call last):
        ...
    ValueError: 価格は0以上にしてください
    """
    if price < 0:
        raise ValueError("価格は0以上にしてください")
    return int(price * (1 + rate))


if __name__ == "__main__":
    import doctest
    doctest.testmod()

>>> の行が実行するコードで、その次の行が期待する結果です。結果の行は、次の >>> か空行が来るまで続くと考えてください。

最後の3行は、このファイルを直接実行したときだけdoctestを動かすための書き方です。if __name__ == "__main__": の意味が気になる方は、こちらで詳しく解説しています。【関連記事】Pythonのname == 'main'って何?初心者が必ず疑問に思うこと

実行してみると何も表示されない

さっそく python price.py で実行してみます。手元のPython 3.11で試したところ、画面には何も表示されませんでした。

壊れているのではと不安になるかもしれませんが、これで正常です。doctestは、すべての例が成功したときは黙っているのが基本の動きです。

本当に動いているのか確かめたいときは、-v を付けて実行します。今度はモジュールとして呼び出す書き方を使ってみましょう。

python -m doctest -v price.py

この書き方なら、ファイルの末尾に doctest.testmod() を書いていなくても動きます。手元では、最後に次のような要約が表示されました。

1 items passed all tests:
   3 tests in price.tax_included
3 tests in 2 items.
3 passed and 0 failed.
Test passed.

3つの例がすべて成功した、という意味です。途中の行では、どのコードを実行して何を期待したかも1つずつ表示されます。

失敗したときの表示を読む

doctestの本当の出番は、例と実際の動きがずれたときです。わざと失敗する例を作って、表示の読み方を確認しておきましょう。

次の関数は、点数の平均を返します。docstringには、平均が80になると書いておきました。

def average(scores):
    """点数の平均を返す。

    >>> average([70, 80, 90])
    80
    """
    return sum(scores) / len(scores)

このファイルをfail.pyとして保存し、python -m doctest fail.py で実行すると、次のように表示されました。

**********************************************************************
File "fail.py", line 4, in fail.average
Failed example:
    average([70, 80, 90])
Expected:
    80
Got:
    80.0
**********************************************************************
1 items had failures:
   1 of   1 in fail.average
***Test Failed*** 1 failures.

ファイル名のパスは短く省略しています。Failed exampleが実行したコード、Expectedが書いておいた結果、Gotが実際の結果です。

原因は、Pythonの割り算 / が、割り切れても小数を返すことでした。人の目では80と80.0は同じに見えますが、doctestは表示された文字を1文字ずつ比べるので、別物として扱います。

ちなみに、失敗があるとき終了コードは1になります。あとで紹介するGitHub Actionsのような自動チェックでも、この終了コードで成功と失敗を見分けられます。

書き方のルールと、つまずきやすいポイント

doctestは手軽な反面、文字の比較ならではのクセがあります。初心者がつまずきやすいところを先に知っておくと安心です。

主なルールと注意点を、表にまとめました。

場面 書き方・注意点
コードの1行目 >>> で始める(>>> のあとに半角スペース)
複数行にまたがるコード 2行目以降は ... で始める
結果に空行を含む 空行の代わりに <BLANKLINE> と書く
結果が何もない 結果の行を書かない(Noneは表示されない)
例外を確かめる Traceback (most recent call last): の行と、最後の例外の行を書く
行末の余計な空白 見えないのに失敗の原因になる

いくつか、補足が必要なものを見ていきます。

例外は、最初と最後の行だけ書けばいい

先ほどのprice.pyでは、マイナスの価格を渡したときにValueErrorが出ることも確かめていました。例外の例では、Traceback (most recent call last): の行と、最後の ValueError: ... の行が大切です。

途中に表示されるファイル名や行番号は、環境によって変わります。そのため、doctestはこの部分を無視してくれるので、例のように ... と書いておけば十分です。

エラーの表示の読み方そのものは、こちらの記事が参考になります。【関連記事】Pythonのエラー文はどこを読めばいい?初心者向けtracebackの見方

小数と辞書は、表示がずれやすい

小数の計算は、doctestでいちばん失敗しやすい場面です。たとえば 0.1 + 0.2 の結果は、0.3ではなく 0.30000000000000004 と表示されます。

doctestの期待値には、この表示をそのまま書く必要があります。あるいは、round() で丸めてから比べるように例を工夫するのもよい方法です。小数がずれる理由は、こちらで詳しく解説しています。【関連記事】Pythonの丸め誤差とは?初心者にもわかる原因と対策

辞書やセットも注意が必要です。辞書は入れた順番どおりに表示されますが、セットは表示の順番が決まっていないため、例が通ったり通らなかったりすることがあります。

セットの結果を確かめたいときは、sorted() で並べ替えてから表示させるのが定番の書き方です。表示が毎回同じになるように例を書く、という意識を持っておくと失敗が減ります。

オプションで比べ方をゆるめる

表示を1文字ずつ比べるのが基本とはいえ、それだと困る場面もあります。そんなときは、オプションフラグで比べ方を少しゆるめられます。

オプションは、例の行末に # doctest: +オプション名 というコメントを付けて指定します。よく使うものを表にまとめました。

オプション 効果
ELLIPSIS 期待値の ... が、どんな文字列にも一致する
NORMALIZE_WHITESPACE 空白や改行の違いを同じものとして扱う
IGNORE_EXCEPTION_DETAIL 例外のメッセージ部分の違いを無視する
SKIP その例を実行しない
FAIL_FAST 最初の失敗で止める

いちばん出番が多いのは、ELLIPSISでしょう。オブジェクトを表示すると、実行のたびに変わるメモリの番地が含まれますが、その部分を ... でまとめられます。

次の例は、手元で実行してすべて成功しました。長いリストを2行に折り返して書けるのは、NORMALIZE_WHITESPACEのおかげです。

def demo():
    """
    >>> object()  # doctest: +ELLIPSIS
    <object object at 0x...>
    >>> list(range(12))  # doctest: +NORMALIZE_WHITESPACE
    [0, 1, 2, 3, 4, 5,
     6, 7, 8, 9, 10, 11]
    """

ファイル全体に同じオプションをかけたいときは、コマンドの -o で指定できます。たとえば python -m doctest -o ELLIPSIS price.py のように書きます。

ただし、ゆるめすぎると何でも通ってしまうテストになります。オプションは、変わってもかまわない部分にだけ使うのがコツです。

テキストファイルやpytestでも動かせる

doctestが読むのは、Pythonファイルの中のdocstringだけではありません。READMEのような説明用のテキストファイルに書いた例も、同じように確かめられます。

たとえば、guide.txtに次のような内容を書いたとします。>>> の行は、ふつうの文章の中に混ぜて書けます。

使い方ガイド

    >>> from price import tax_included
    >>> tax_included(500)
    550

これを python -m doctest -v guide.txt で実行すると、手元では2つの例がすべて成功しました。拡張子が.pyでないファイルは、テキストとして中の例を探してくれます。

pytestと一緒に使う

すでにpytestでテストを書いている方も多いかもしれません。pytestには、doctestをまとめて実行する機能が用意されています。

pytestの公式ドキュメントによると、pytest --doctest-modules と指定すると、モジュールのdocstringに書いた例もテストとして集めてくれます。テキストファイルの例は、--doctest-glob で対象のファイル名のパターンを指定できます。

ふだんのテストとdoctestを1つのコマンドで流せるので、確認のし忘れがなくなります。pytestの基本は、こちらの記事でまとめています。【関連記事】pytest入門|Pythonでテストを書く基本を初心者向けに解説

doctestとふつうのテストの使い分け

ここまで読んで、それならテストは全部doctestで書けばいいのでは、と思った方もいるかもしれません。けれども、doctestには得意なことと苦手なことがあります。

違いを表で比べてみましょう。

項目 doctest pytestなどのテスト
主な目的 使用例が正しいことを保つ 動きを細かく確かめる
書く場所 docstringやテキストファイル テスト用の別ファイル
読み手 関数を使う人 開発する人
比べ方 表示された文字列 値そのもの
向いている例 小さな計算、文字列の変換 ファイル、通信、データベース
準備や後片付け 苦手 得意

doctestは、使う人に見せたい代表的な例を数個だけ書くのに向いています。境目の値や、たくさんの組み合わせを試すのは、ふつうのテストに任せたほうが読みやすくなります。

私は10年ほどエンジニアとして開発に関わってきましたが、READMEに書いたサンプルコードが古くなっていて、新しく入ったメンバーがそのとおりに動かして詰まる、という場面を何度も見てきました。それ以来、外に見せる使用例だけはdoctestで毎回確かめるようにしています。

一方で、docstringに例を詰め込みすぎて、説明がテストに埋もれてしまったこともあります。例は3つ前後に絞り、細かい確認はテストファイルへ、と分けるようになってから、説明もテストもぐっと読みやすくなりました。

doctestを習慣にするコツ

最後に、doctestを無理なく続けるためのコツをまとめておきます。どれも今日から始められることばかりです。

1つめは、関数を書いたらまず使用例を1つだけ書くことです。完璧を目指さず、いちばん基本の呼び出し方だけで十分です。

2つめは、例を書いたら一度わざと失敗させてみることです。期待値を1文字変えて失敗の表示が出れば、doctestが本当に動いている証拠になります。

3つめは、実行を自動にすることです。pytestの設定に組み込んだり、GitHub Actionsで毎回実行したりすれば、確認を忘れる心配がなくなります。

コードを直すたびに、説明まで自動で確かめてくれる。そう考えると、doctestはかなり頼もしい相棒だと思いませんか。

まとめ

doctestは、docstringに書いた対話モード風の使用例を、そのままテストとして実行できる標準ライブラリです。インストール不要で、python -m doctest -v ファイル名 だけで試せます。

覚えておきたいのは、結果を表示された文字で比べるという点です。小数やセット、メモリの番地のように表示が揺れるものは、例を工夫するかオプションで比べ方を調整しましょう。

説明が正しいままであることは、コードを使う人への何よりの親切です。まずは、よく使う関数1つにdoctestを書いてみてください。

ここまでお読みいただきありがとうございました。

参考情報

次のアクション

記事で学んだ内容を実際に動かしてみよう

Python WebAcademyでは、ブラウザ上でコードを書きながら基礎から実践まで体系的に学べます。

Python WebAcademyの学習画面

あわせて読む

関連記事

ブログ一覧へ

Python学習ロードマップ

まずはこの3講座から

記事で気になったテーマを、順番に手を動かしながら学べます。

ロードマップを見る