最近自作Pythonライブラリでがっつりdoctestを使い始めたので記事にまとめておきます。
doctestってなに?
dostring内に書くコードサンプルを実際にPythonで動かしてエラーにならないことや返却値が正しいかどうかをチェックすることができる機能です。
※docstringについては必要に応じて以下の記事などをご確認ください。
※Rustなどにも似たような機能がビルトインで入っています。
doctestを書くとなにが嬉しいの?
docstringにコードサンプルが載っているとユーザーがエディタ上などでさくっと使い方を確認できてユーザーフレンドリーです。
一方で書いただけだとそのコードサンプルはテストやLintでチェックされるわけではありませんので正常に動作しないケースが発生し得ます。書いたときは動いていても日々のアップデートでいつの間にか動かなくなってしまうこともあるかもしれません。
コードサンプルが動いてくれないとその資料を見ているユーザーに取って開発体験が良くありません。
そこで通常の単体テストのようにdocstring内のコードサンプルで実行・返却値の確認などのテストが実行されることでコードサンプルが常に動いていることを担保することができます。もちろん通常のテストやLintなどと同様にCI/CDのフローに組み込むことも可能です。
また、単体テストなどにはpytestを使われている方が多いと思いますが、pytestはdoctestの機能を有しているためpytestをお使いの方は既存のテストと同じ感覚ですぐにdoctestを使い始めることができます。本記事でもpytestで動かす形で進めていきます。
※コメントにて @shiracamusさんがご指摘くださいましたが、pytestを使わなくてもビルトインのみでさくっと使うことができます。お好きな方をご利用ください。
この記事で使うもの
- Python 3系環境(3.6以降を想定しています)
- pytest==6.2.5
pytestでのdoctestの動かし方
基本的にpytestのコマンドの引数に--doctest-modules
と加えるだけです。これで通常の単体テストではなくdocstring内のサンプルコードがテストとして実行され、通常の単体テストと同様にテスト結果を得ることができます。
他のpytestの引数やpytestのプラグインなどもそのまま動きます。
doctestの書き方
以降の節ではdoctestの書き方について触れていきます。なお、本記事ではdocstringはNumPyスタイルを前提として進めていきます。
まずはPandasのコードを軽く見てみる
doctestがどんな雰囲気なのかを先に軽く確認するため他のライブラリの実装を確認しておきます。
PythonコミュニティではNumPyスタイルでdocstringが書かれたライブラリが色々とありますが、代表的(且つdocstringもしっかり書かれている)なライブラリのPandasのものを見てみましょう。
データフレームのコードでshape属性のdocstringを見ると以下のようになっています。
...
@property
def shape(self) -> tuple[int, int]:
"""
Return a tuple representing the dimensionality of the DataFrame.
See Also
--------
ndarray.shape : Tuple of array dimensions.
Examples
--------
>>> df = pd.DataFrame({'col1': [1, 2], 'col2': [3, 4]})
>>> df.shape
(2, 2)
>>> df = pd.DataFrame({'col1': [1, 2], 'col2': [3, 4],
... 'col3': [5, 6]})
>>> df.shape
(2, 3)
"""
...
サンプルコードは上記のコードのExamples
セクション部分が該当します。
なお、このフォーマットで書かれた場合VS Codeなどで対象のインターフェイスにマウスオーバーした際などに以下のようにちゃんとシンタックスハイライトなどが以下のように反映された状態で表示されます。
以降の節で詳細を一つずつ触れていきます。
NumPyスタイルのdocstringではExamplesのセクションに書かれることが多い
NumPyスタイルのdocstringではコードサンプル部分はExampleセクション以降に書かれることが多いようです。
例えば(関数は適当ですが)以下のようにExamples
セクションを追加して書き進めていく形になります。
def get_int_sum(x: int, y: int) -> int:
"""
引数に指定された2つの整数の合計を取得する。
Parameters
----------
x : int
1つ目の整数値。
y : int
2つ目の整数値。
Returns
-------
result : int
結果の合計値。
Examples
--------
※ここにコードサンプルを書いていきます...
"""
return x + y
コード実行行は先頭に>>>を付ける
コードサンプルで実行して欲しい行には先頭に>>>
を付けて書きます。
def get_int_sum(x: int, y: int) -> int:
"""
引数に指定された2つの整数の合計を取得する。
Parameters
----------
x : int
1つ目の整数値。
y : int
2つ目の整数値。
Returns
-------
result : int
結果の合計値。
Examples
--------
>>> result: int = get_int_sum(x=10, y=20)
"""
return x + y
これでdoctestでresult: int = get_int_sum(x=10, y=20)
というコードが実行される形になります。試しにpytestで動かしてみます。上記のコードはsample
というフォルダにてsample_1.py
というモジュール名で追加されている想定で進めます。
$ pytest ./sample/ --doctest-modules -v -s
=============================== test session starts ===============================
...
collected 1 item
sample/sample_1.py::sample.sample_1.get_int_sum PASSED
================================ 1 passed in 0.11s ================================
テストが実行されて1件のテストを通った・・・と表示されました。この辺の挙動はpytestの通常の単体テスト等と同様です。
実行行は1つだけではなく複数の行を実行していくことが可能です。各行の変数なども保持される形になります(この辺はインタラクティブシェルやJupyterなどと同じ感覚です)。
def get_int_sum(x: int, y: int) -> int:
"""
引数に指定された2つの整数の合計を取得する。
Parameters
----------
x : int
1つ目の整数値。
y : int
2つ目の整数値。
Returns
-------
result : int
結果の合計値。
Examples
--------
>>> result_1: int = get_int_sum(x=10, y=20)
>>> result_2: int = get_int_sum(x=result_1, y=20)
"""
return x + y
試しにテストがわざと失敗するように、引数に整数ではなく文字列を指定してpytestを実行してみましょう。
def get_int_sum(x: int, y: int) -> int:
"""
引数に指定された2つの整数の合計を取得する。
Parameters
----------
x : int
1つ目の整数値。
y : int
2つ目の整数値。
Returns
-------
result : int
結果の合計値。
Examples
--------
>>> result: int = get_int_sum(x=10, y='Hello!')
"""
return x + y
pytestのコマンドを実行してみるとテストは通らず、失敗したテストの詳細を確認することができます。
$ pytest ./sample/ --doctest-modules -v -s
=============================== test session starts ===============================
...
collected 1 item
sample/sample_1.py::sample.sample_1.get_int_sum FAILED
==================================== FAILURES =====================================
__________________________ [doctest] sample.sample_1.get_int_sum __________________________
010 2つ目の整数値。
011
012 Returns
013 -------
014 result : int
015 結果の合計値。
016
017 Examples
018 --------
019 >>> result: int = get_int_sum(x=10, y='Hello!')
UNEXPECTED EXCEPTION: TypeError("unsupported operand type(s) for +: 'int' and 'str'",)
Traceback (most recent call last):
...
File "<doctest sample.sample_1.get_int_sum[0]>", line 1, in <module>
File ".../sample/sample_1.py", line 21, in get_int_sum
return x + y
TypeError: unsupported operand type(s) for +: 'int' and 'str'
.../sample/sample_1.py:19: UnexpectedException
============================= short test summary info =============================
FAILED sample/sample_1.py::sample.sample_1.get_int_sum
================================ 1 failed in 0.14s ================================
多少通常のpytestの単体テストとは表示が異なりますが、大体似たような雰囲気でエラー内容やエラーの箇所などが表示されます。
返却値の行には先頭に何も付けずに書く
Jupyterの[Out]
部分のアウトプットなどと同じような感じで、返却値などが標準出力に表示される場合には>>>
などを先頭に加えずに記述します。
以下のコードサンプルではget_int_sum
関数の返却値として30
を指定しています。
def get_int_sum(x: int, y: int) -> int:
"""
引数に指定された2つの整数の合計を取得する。
Parameters
----------
x : int
1つ目の整数値。
y : int
2つ目の整数値。
Returns
-------
result : int
結果の合計値。
Examples
--------
>>> get_int_sum(x=10, y=20)
30
"""
return x + y
この値はdoctsetでチェックされます。つまりget_int_sum
関数の実行結果が30になっていないとテストが失敗します。
試しに返却値の指定を40としてわざと失敗するようにしてpytestを動かしてみます。
def get_int_sum(x: int, y: int) -> int:
"""
引数に指定された2つの整数の合計を取得する。
Parameters
----------
x : int
1つ目の整数値。
y : int
2つ目の整数値。
Returns
-------
result : int
結果の合計値。
Examples
--------
>>> get_int_sum(x=10, y=20)
40
"""
return x + y
$ pytest ./sample/ --doctest-modules -v -s
=============================== test session starts ===============================
...
collected 1 item
sample/sample_1.py::sample.sample_1.get_int_sum FAILED
==================================== FAILURES =====================================
__________________________ [doctest] sample.sample_1.get_int_sum __________________________
010 2つ目の整数値。
011
012 Returns
013 -------
014 result : int
015 結果の合計値。
016
017 Examples
018 --------
019 >>> get_int_sum(x=10, y=20)
Expected:
40
Got:
30
.../sample/sample_1.py:19: DocTestFailure
============================= short test summary info =============================
FAILED sample/sample_1.py::sample.sample_1.get_int_sum
================================ 1 failed in 0.13s ================================
想定値(40)と実際に返却値として得られた値(30)が違っているよ、といった具合に表示されてテストが失敗していることが分かります。
このチェックはJupyterなどと同様にprint文などを通さなくても変数単体で入力行に記述しても動作します。
例えば以下のようにresult
変数単体で入力行に指定してみてもテストを通ります。
def get_int_sum(x: int, y: int) -> int:
"""
引数に指定された2つの整数の合計を取得する。
Parameters
----------
x : int
1つ目の整数値。
y : int
2つ目の整数値。
Returns
-------
result : int
結果の合計値。
Examples
--------
>>> result: int = get_int_sum(x=10, y=20)
>>> result
30
"""
return x + y
$ pytest ./sample/ --doctest-modules -v -s
...
sample/sample_1.py::sample.sample_1.get_int_sum PASSED
================================ 1 passed in 0.12s ================================
1つのdocstring内で複数の返却値などを指定することもできます。間に空行などを入れても問題なく動作します。
def get_int_sum(x: int, y: int) -> int:
"""
引数に指定された2つの整数の合計を取得する。
Parameters
----------
x : int
1つ目の整数値。
y : int
2つ目の整数値。
Returns
-------
result : int
結果の合計値。
Examples
--------
>>> result: int = get_int_sum(x=10, y=20)
>>> result
30
>>> result = get_int_sum(x=result, y=20)
>>> result
50
"""
return x + y
...
sample/sample_1.py::sample.sample_1.get_int_sum PASSED
================================ 1 passed in 0.12s ================================
複数行に処理を繋げる場合には先頭に...と書く
PEP8準拠などの都合で1行の長さ制限を設けている場合に関数呼び出し箇所などで改行を入れたくなることが結構あります。そういった場合には先頭に>>>
の代わりに...
を記述します。
def get_int_sum(x: int, y: int) -> int:
"""
引数に指定された2つの整数の合計を取得する。
Parameters
----------
x : int
1つ目の整数値。
y : int
2つ目の整数値。
Returns
-------
result : int
結果の合計値。
Examples
--------
>>> result: int = get_int_sum(
... x=10, y=20)
>>> result
30
"""
return x + y
テストを動かしてみても動作します。
$ pytest ./sample/ --doctest-modules -v -s
sample/sample_1.py::sample.sample_1.get_int_sum PASSED
================================ 1 passed in 0.09s ================================
関数定義などの基本的に改行が必要な箇所でも同様に...
の記述を行うことが必要になります。
def get_int_sum(x: int, y: int) -> int:
"""
引数に指定された2つの整数の合計を取得する。
Parameters
----------
x : int
1つ目の整数値。
y : int
2つ目の整数値。
Returns
-------
result : int
結果の合計値。
Examples
--------
>>> def get_x() -> int:
... return 10
>>> result: int = get_int_sum(
... x=get_x(), y=20)
>>> result
30
"""
return x + y
sample/sample_1.py::sample.sample_1.get_int_sum PASSED
================================ 1 passed in 0.12s ================================
...
を書くべき箇所を>>>
としてしまうとシンタックスエラーになります。これはPythonが>>>
で始まる行単位で個別に動かしていくことに起因するためです(分割された時に正しく動くコードになっていないと怒られてしまいます)。
以下のコードではresult: int = get_int_sum(
という行単体で実行されるため、閉じ括弧が無いといった具合でシンタックスエラーとなってテストが失敗します。
def get_int_sum(x: int, y: int) -> int:
"""
引数に指定された2つの整数の合計を取得する。
Parameters
----------
x : int
1つ目の整数値。
y : int
2つ目の整数値。
Returns
-------
result : int
結果の合計値。
Examples
--------
>>> result: int = get_int_sum(
>>> x=10, y=20)
>>> result
30
"""
return x + y
...
==================================== FAILURES =====================================
______________________ [doctest] sample.sample_1.get_int_sum ______________________
010 2つ目の整数値。
011
012 Returns
013 -------
014 result : int
015 結果の合計値。
016
017 Examples
018 --------
019 >>> result: int = get_int_sum(
UNEXPECTED EXCEPTION: SyntaxError('unexpected EOF while parsing', ('<doctest sample.sample_1.get_int_sum[0]>', 1, 27, 'result: int = get_int_sum(\n'))
Traceback (most recent call last):
...
File "<doctest sample.sample_1.get_int_sum[0]>", line 1
result: int = get_int_sum(
^
SyntaxError: unexpected EOF while parsing
.../sample/sample_1.py:19: UnexpectedException
============================= short test summary info =============================
FAILED sample/sample_1.py::sample.sample_1.get_int_sum
================================ 1 failed in 0.13s ================================
行末のエスケープを指定した際には注意
コードの途中で通常は改行ができない箇所で行末に\の記号を入れて改行を行う・・・というケースもあると思います。
この場合には先頭に...
の指定が入っているとテストがエラーになってしまいます。というのも\による行末のエスケープの指定は「改行を無いものと判定する」挙動をするため、実行対象のコード内に...
の記述が入り込んでしまって正常な挙動にならなくなります。
例えば以下のようなdoctestの記述だとテストが失敗します。
def get_int_sum(x: int, y: int) -> int:
"""
引数に指定された2つの整数の合計を取得する。
Parameters
----------
x : int
1つ目の整数値。
y : int
2つ目の整数値。
Returns
-------
result : int
結果の合計値。
Examples
--------
>>> result: int = \
... get_int_sum(x=10, y=20)
>>> result
30
"""
return x + y
...
==================================== FAILURES =====================================
______________________ [doctest] sample.sample_1.get_int_sum ______________________
010 2つ目の整数値。
011
012 Returns
013 -------
014 result : int
015 結果の合計値。
016
017 Examples
018 --------
019 >>> result: int = ... get_int_sum(x=10, y=20)
UNEXPECTED EXCEPTION: SyntaxError('invalid syntax', ('<doctest sample.sample_1.get_int_sum[0]>', 1, 37, 'result: int = ... get_int_sum(x=10, y=20)\n'))
Traceback (most recent call last):
...
File "<doctest sample.sample_1.get_int_sum[0]>", line 1
result: int = ... get_int_sum(x=10, y=20)
^
SyntaxError: invalid syntax
.../sample/sample_1.py:19: UnexpectedException
============================= short test summary info =============================
FAILED sample/sample_1.py::sample.sample_1.get_int_sum
================================ 1 failed in 0.13s ================================
トレースバックを見てみてもresult: int = ... get_int_sum(x=10, y=20)
といったように間に...
が入ったコードが実行されてしまっていることが分かります。
このエラーを避けるためにはそのまま...
を先頭に加えずに書きます。
def get_int_sum(x: int, y: int) -> int:
"""
引数に指定された2つの整数の合計を取得する。
Parameters
----------
x : int
1つ目の整数値。
y : int
2つ目の整数値。
Returns
-------
result : int
結果の合計値。
Examples
--------
>>> result: int = \
get_int_sum(x=10, y=20)
>>> result
30
"""
return x + y
これで正常に動いてくれます。
sample/sample_1.py::sample.sample_1.get_int_sum PASSED
================================ 1 passed in 0.13s ================================
返却値を返したり標準出力がされる関数などには注意
返却値を使わないのだけれどもテスト時に事前の設定用などに呼び出しが必要・・・といった類の関数を入力行に指定する際には注意が必要です。
例えばJupyterなどで作業している時にmatplotlibなどでも軸やFigureなどの返却値は返ってくるもののそれらは特に使わない・・・といったケースがあると思いますが、あのようなケースがdoctest中にあるとdoctestで引っかかってしまうケースが発生してきます。
以下のコードサンプルではsetup
という関数をコードサンプルの最初に呼び出しています。仮に正常に処理が通った場合にTrueが返る関数としましょう。実際のコードサンプルでは返却値は使わないので返却値を変数に入れたりなどはしていません。
def get_int_sum(x: int, y: int) -> int:
"""
引数に指定された2つの整数の合計を取得する。
Parameters
----------
x : int
1つ目の整数値。
y : int
2つ目の整数値。
Returns
-------
result : int
結果の合計値。
Examples
--------
>>> def setup() -> bool:
... return True
>>> setup()
>>> result: int = get_int_sum(x=10, y=20)
>>> result
30
"""
return x + y
しかしながらテストを実行してみるとこのコードサンプルだと引っかかってしまいます。
==================================== FAILURES =====================================
______________________ [doctest] sample.sample_1.get_int_sum ______________________
012 Returns
013 -------
014 result : int
015 結果の合計値。
016
017 Examples
018 --------
019 >>> def setup() -> bool:
020 ... return True
021 >>> setup()
Expected nothing
Got:
True
.../sample/sample_1.py:21: DocTestFailure
============================= short test summary info =============================
FAILED sample/sample_1.py::sample.sample_1.get_int_sum
================================ 1 failed in 0.12s ================================
>>> setup()
の行で引っかかっていることが分かります。そちらで想定値はnothing、つまり返却値(出力される値)は何もない想定ではあるものの、実際にはTrueが出力されている・・・という状態になります。
このように「使わないけど返却値が返る関数」などを使いたい場合には_
を変数として指定しておくと良いかなと思います。Python界隈ではこの変数名は基本的に設定されても使わないといったケースなどに利用されます。
先ほどのコードを>>> _ = setup()
と書き換えることでテストが通ることを確認できます。
def get_int_sum(x: int, y: int) -> int:
"""
引数に指定された2つの整数の合計を取得する。
Parameters
----------
x : int
1つ目の整数値。
y : int
2つ目の整数値。
Returns
-------
result : int
結果の合計値。
Examples
--------
>>> def setup() -> bool:
... return True
>>> _ = setup()
>>> result: int = get_int_sum(x=10, y=20)
>>> result
30
"""
return x + y
sample/sample_1.py::sample.sample_1.get_int_sum PASSED
================================ 1 passed in 0.12s ================================
外部モジュールの読み込みなども効く
コードサンプル中で外部のモジュールをimportして使ったりすることもできます。
def get_int_sum(x: int, y: int) -> int:
"""
引数に指定された2つの整数の合計を取得する。
Parameters
----------
x : int
1つ目の整数値。
y : int
2つ目の整数値。
Returns
-------
result : int
結果の合計値。
Examples
--------
>>> import math
>>> result: int = get_int_sum(x=math.floor(10.5), y=20)
>>> result
30
"""
return x + y
sample/sample_1.py::sample.sample_1.get_int_sum PASSED
================================ 1 passed in 0.13s ================================
独自クラスなどでは__repr__のダンダーメソッドを書いておくと快適
独自に定義したクラスのインスタンス自体をテストの出力として設定したい場合がたまにあると思います。
たとえば以下のPoint
という自前で定義したクラスで試してみます。
class Point:
_x: int
_y: int
def __init__(self, x: int, y: int) -> None:
"""
XとY座標の値を扱うクラス。
Parameters
----------
x : int
X座標。
y : int
Y座標。
Examples
--------
>>> point: Point = Point(x=10, y=20)
>>> point
"""
self._x = x
self._y = y
テストを流してみると以下のように引っかかります。
==================================== FAILURES =====================================
____________________ [doctest] sample.sample_1.Point.__init__ _____________________
011 ----------
012 x : int
013 X座標。
014 y : int
015 Y座標。
016
017 Examples
018 --------
019 >>> point: Point = Point(x=10, y=20)
020 >>> point
Expected nothing
Got:
<sample.sample_1.Point object at 0x7f2bc1a7e9b0>
.../sample/sample_1.py:20: DocTestFailure
============================= short test summary info =============================
FAILED sample/sample_1.py::sample.sample_1.Point.__init__
================================ 1 failed in 0.14s ================================
0x7f2bc1a7e9b0
といった値はid
関数で取れるメモリアドレス的な値になるため実行の度に変わってしまいます。こういった値は出力用で指定するには厄介です。出来たらPoint(x=10, y=20)
みたいな属性に応じた表示だとテストを書くのが楽です。
そういった場合には__repr__
のダンダーメソッドをクラスで定義しておくと任意のフォーマットで出力するように調整することができます。
※ダンダーメソッド(Dunder methods)はPythonのアンダースコアが2つ指定される特殊なメソッドのことです(double underscore methodの略)。詳しくは以下の記事などをご確認ください。
以下のコードでは__repr__
の記述を追加してPoint(x=10, y=20)
の形式で出力されているようにしているため、コードサンプル部分でPoint(x=10, y=20)
という出力値を指定していればテストを通ってくれます。
class Point:
_x: int
_y: int
def __init__(self, x: int, y: int) -> None:
"""
XとY座標の値を扱うクラス。
Parameters
----------
x : int
X座標。
y : int
Y座標。
Examples
--------
>>> point: Point = Point(x=10, y=20)
>>> point
Point(x=10, y=20)
"""
self._x = x
self._y = y
def __repr__(self) -> str:
return f'Point(x={self._x}, y={self._y})'
...
sample/sample_1.py::sample.sample_1.Point.__init__ PASSED
================================ 1 passed in 0.13s ================================
doctest内で例外を発生させる記述に付いて
assert_raises的なことをしたい場合、そちらも対応しているようで@shiracamusさんがコメントでリンクなどをご共有くださったためそちらも合わせてご確認ください!
関数やメソッド以外の箇所のdocstringでも対象となる
doctestの実行対象は関数やメソッドに記載されているもの以外のdocstringも対象となります。例えばクラス自体のdocstringなどもテスト対象となります。
クラスであればVS Codeではコンストラクタのdocstringはコンストラクタの呼び出しを書いている際に表示され、クラス自体のdocstringはクラスに対してマウスオーバーした際に表示されるなど表示されるタイミングが異なります。そのため両方にコードサンプルを書いておくというのもアリだと思います(コンストラクタにはインスタンス化時のコードサンプル、クラス自体にはクラスのインターフェイス全体的なコードサンプルを書くなど)。
例えば以下のような形になります。
class Point:
"""
XとY座標の値を扱うクラス。
Examples
--------
>>> point: Point = Point(x=10, y=20)
>>> point
Point(x=10, y=20)
>>> point.x
10
"""
_x: int
_y: int
def __init__(self, x: int, y: int) -> None:
"""
XとY座標の値を扱うクラス。
Parameters
----------
x : int
X座標。
y : int
Y座標。
Examples
--------
>>> point: Point = Point(x=10, y=20)
>>> point
Point(x=10, y=20)
"""
self._x = x
self._y = y
@property
def x(self) -> int:
"""
設定されているX座標の属性値を取得する。
Returns
-------
x : int
設定されているX座標の属性値。
Examples
--------
>>> point: Point = Point(x=10, y=20)
>>> point.x
10
"""
return self._x
def __repr__(self) -> str:
return f'Point(x={self._x}, y={self._y})'
テストを実行してみるとクラス自体のdocstringの分も含めてテストが実行されていることが確認できます。
...
sample/sample_1.py::sample.sample_1.Point PASSED
sample/sample_1.py::sample.sample_1.Point.__init__ PASSED
sample/sample_1.py::sample.sample_1.Point.x PASSED
================================ 3 passed in 0.13s ================================
編集の際にはマルチカーソルを使うと楽
余談気味となりますが、>>>
や...
の記述などは複数行同じ記述となったりすることが多いためVS Codeなどでマルチカーソル等を使っておくと編集が短時間で終わって楽です。
既にマルチカーソルを普段から使われている方が多いと思いますが、一応参考としてリンクを貼っておきます。
実はJupyterだとコードの先頭に>>>などがあっても動く
さらに余談ですがJupyterは実行セルで>>>
や...
の記述があっても動きます(ただし入れているLintなどは引っかかったりします)。
コードサンプルなどを直接コピペなどして動かしたりする際などに便利な時があるかもしれません。
以下のスクショはVS Code上のJupyterですが、セルを実行してもエラーにならずに動いていることを確認できます。
実際にdoctestを反映してみたプロジェクト
自作のPythonでSVGベースのフロントエンドを書けることを目指してちまちま作っていっているapyscというライブラリで今回のdoctestをpublicなインターフェイスで全体的に追加したりGitHub Actionsでのデプロイのワークフローでのチェックに組み込んだりしています。
実際のコードでのdoctestを確認したい・・・といった場合にご利用ください。
参考文献・参考サイトまとめ