関数の説明に使用例を書いておいたのに、あとで中身を直したら、その例が嘘になっていた。そんな経験はありませんか。
説明文はプログラムと違って、間違っていてもエラーを出してくれません。だからこそ、気づかないうちに古くなってしまいます。
そこで役に立つのが、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を書いてみてください。
ここまでお読みいただきありがとうございました。