PythonでDoctestを作成する方法
序章
ドキュメントとテストは、すべての生産的なソフトウェア開発プロセスのコアコンポーネントです。 コードが完全に文書化され、テストされていることを確認すると、プログラムが期待どおりに実行されるだけでなく、プログラマー間のコラボレーションやユーザーの採用もサポートされます。 プログラマーは、最初にドキュメントを作成し、次にテストを行ってから、最終的にコードを作成することで、十分なサービスを受けることができます。 このようなプロセスに従うことで、コーディングしている関数(たとえば)がよく考えられ、考えられるエッジケースに対処できるようになります。
Pythonの標準ライブラリには、doctest
と呼ばれるテストフレームワークモジュールが付属しています。 doctestモジュールは、インタラクティブなPythonセッションのように見えるコメント内のテキストをPythonコードでプログラムで検索します。 次に、モジュールはそれらのセッションを実行して、doctestによって参照されるコードが期待どおりに実行されることを確認します。
さらに、doctest
は、コードのドキュメントを生成し、入出力の例を提供します。 Python標準ライブラリのドキュメントで説明されているように、ドキュメントテストの作成方法に応じて、これは「「リテラシーテスト」または「実行可能ドキュメント」」のいずれかに近い可能性があります。
前提条件
Python 3をインストールし、コンピューターまたはサーバーにプログラミング環境をセットアップする必要があります。 プログラミング環境をセットアップしていない場合は、ローカルプログラミング環境またはサーバー上のプログラミング環境のインストールおよびセットアップガイドを参照して、オペレーティングに適したものにすることができます。システム(Ubuntu、CentOS、Debianなど)
Doctestの構造
Python doctestは、コメントのように記述され、doctestの上部と下部に一連の3つの引用符("""
)が連続して表示されます。
doctestは、関数の例と期待される出力を使用して記述される場合がありますが、関数の目的についてのコメントも含めることが望ましい場合があります。 コメントを含めることで、プログラマーとしてのあなたが目標を明確にし、コードを読む将来の人がそれをよく理解できるようになります。 コードを読む将来のプログラマーはあなたかもしれないことを忘れないでください。
情報:このチュートリアルのサンプルコードに従うには、python3
コマンドを実行して、ローカルシステムでPythonインタラクティブシェルを開きます。 次に、>>>
プロンプトの後に例を追加して、例をコピー、貼り付け、または編集できます。
以下は、2つの数値を加算するadd(a, b)
などの関数のdoctestの数学的な例です。
"""
Given two integers, return the sum.
>>> add(2, 3)
5
"""
この例では、説明の行と、入力値に2つの整数を使用するadd()
関数の1つの例があります。 将来、関数で3つ以上の整数を追加できるようにする場合は、関数の入力に一致するようにdoctestを修正する必要があります。
これまでのところ、このdoctestは人間にとって非常に読みやすいものです。 関数に出入りする各変数を説明するために、機械可読パラメーターと戻り値の説明を含めることで、このdocstringをさらに繰り返すことができます。
ここでは、関数に渡される2つの引数と返される値のdocstringを追加します。 docstringは、各値(パラメーターa
、パラメーターb
、および戻り値)のデータ型を記録します。この場合、これらはすべて整数です。
"""
Given two integers, return the sum.
:param a: int
:param b: int
:return: int
>>> add(2, 3)
5
"""
これで、このdoctestを関数に組み込んでテストする準備が整いました。
Doctestを関数に組み込む
Doctestは、def
ステートメントの後、関数のコードの前にある関数内にあります。 これは関数の初期定義に従うため、Pythonの規則に従ってインデントされます。
この短い関数は、doctestがどのように組み込まれるかを示します。
def add(a, b):
"""
Given two integers, return the sum.
:param a: int
:param b: int
:return: int
>>> add(2, 3)
5
"""
return a + b
この短い例では、プログラムにこの1つの関数しかないため、doctestモジュールをインポートし、doctestを実行するためのcallステートメントを用意する必要があります。
関数の前後に次の行を追加します。
import doctest
...
doctest.testmod()
この時点で、今すぐプログラムファイルに保存するのではなく、Pythonシェルでテストしてみましょう。 python3
コマンド(または仮想シェルを使用している場合はpython
)を使用して、選択したコマンドラインターミナル(IDEターミナルを含む)でPython3シェルにアクセスできます。
- python3
このルートに行く場合、ENTER
を押すと、次のような出力が表示されます。
OutputType "help", "copyright", "credits" or "license" for more information.
>>>
>>>
プロンプトの後にコードの入力を開始できます。
doctest、docstrings、およびdoctestを呼び出すための呼び出しを含むadd()
関数を含む、完全なサンプルコードを次に示します。 Pythonインタープリターに貼り付けて、試してみることができます。
import doctest
def add(a, b):
"""
Given two integers, return the sum.
:param a: int
:param b: int
:return: int
>>> add(2, 3)
5
"""
return a + b
doctest.testmod()
コードを実行すると、次の出力が表示されます。
OutputTestResults(failed=0, attempted=1)
これは、プログラムが期待どおりに実行されたことを意味します。
上記のプログラムを変更して、return a + b
行が代わりにreturn a * b
になるようにすると、関数が整数を乗算してその積を返すように変更され、失敗の通知が届きます。
Output**********************************************************************
File "__main__", line 9, in __main__.add
Failed example:
add(2, 3)
Expected:
5
Got:
6
**********************************************************************
1 items had failures:
1 of 1 in __main__.add
***Test Failed*** 1 failures.
TestResults(failed=1, attempted=1)
上記の出力から、a
とb
を加算する代わりに乗算して、
複数の例を試してみることをお勧めします。 変数a
とb
の両方に0
の値が含まれている例を試してみて、プログラムを+
演算子で加算に戻します。
import doctest
def add(a, b):
"""
Given two integers, return the sum.
:param a: int
:param b: int
:return: int
>>> add(2, 3)
5
>>> add(0, 0)
0
"""
return a + b
doctest.testmod()
これを実行すると、Pythonインタープリターから次のフィードバックを受け取ります。
OutputTestResults(failed=0, attempted=2)
ここで、出力は、doctestがadd(2, 3)
とadd(0, 0)
の両方の行で2つのテストを試行し、両方が合格したことを示しています。
ここでも、+
演算子ではなく*
演算子を乗算に使用するようにプログラムを変更すると、2番目の例であるため、doctestモジュールを操作するときにエッジケースが重要であることがわかります。 add(0, 0)
の値は、加算でも乗算でも同じ値を返します。
import doctest
def add(a, b):
"""
Given two integers, return the sum.
:param a: int
:param b: int
:return: int
>>> add(2, 3)
5
>>> add(0, 0)
0
"""
return a * b
doctest.testmod()
次の出力が返されます。
Output**********************************************************************
File "__main__", line 9, in __main__.add
Failed example:
add(2, 3)
Expected:
5
Got:
6
**********************************************************************
1 items had failures:
1 of 2 in __main__.add
***Test Failed*** 1 failures.
TestResults(failed=1, attempted=2)
プログラムを変更すると、失敗する例は1つだけですが、前と同じように完全に説明されています。 add(2, 3)
の例ではなくadd(0, 0)
の例から始めた場合、プログラムの小さなコンポーネントが変更されたときに失敗する可能性があることに気づかなかったかもしれません。
プログラミングファイルのDoctests
これまで、Pythonインタラクティブターミナルで例を使用してきました。 これを、1つの単語の母音の数をカウントするプログラミングファイルで使用してみましょう。
プログラムでは、プログラミングファイルの下部にあるif __name__ == "__main__":
句でdoctestモジュールをインポートして呼び出すことができます。
新しいファイル— counting_vowels.py
—をテキストエディタで作成します。コマンドラインで次のようにnano
を使用できます。
- nano counting_vowels.py
関数count_vowels
を定義し、word
のパラメーターを関数に渡すことから始めることができます。
def count_vowels(word):
関数の本体を書く前に、doctestで関数に何をさせたいかを説明しましょう。
def count_vowels(word):
"""
Given a single word, return the total number of vowels in that single word.
これまでのところ、かなり具体的です。 パラメータword
のデータ型と、返されるデータ型を使用して、これを具体化してみましょう。 前者の場合は文字列であり、後者の場合は整数です。
def count_vowels(word):
"""
Given a single word, return the total number of vowels in that single word.
:param word: str
:return: int
次に、例を見つけましょう。 母音を含む1つの単語を考えて、それをdocstringに入力します。
ペルーの都市を表す'Cusco'
という単語を選びましょう。 「クスコ」には母音がいくつありますか? 英語では、母音はa
、e
、i
、o
、およびu
と見なされることがよくあります。 したがって、ここではu
とo
を母音として数えます。
Cuscoのテストと、整数としての2
の戻り値をプログラムに追加します。
def count_vowels(word):
"""
Given a single word, return the total number of vowels in that single word.
:param word: str
:return: int
>>> count_vowels('Cusco')
2
繰り返しますが、複数の例を用意することをお勧めします。 母音を増やした別の例を見てみましょう。 フィリピンの都市には'Manila'
を使用します。
def count_vowels(word):
"""
Given a single word, return the total number of vowels in that single word.
:param word: str
:return: int
>>> count_vowels('Cusco')
2
>>> count_vowels('Manila')
3
"""
これらのdoctestは見栄えがよく、プログラムをコーディングできるようになりました。
母音数を保持するために、変数 —total_vowels
を初期化することから始めます。 次に、 for loop を作成して、word
string の文字を反復処理し、条件付きステートメントを含めてチェックします。各文字が母音であるかどうか。 ループ全体で母音数を増やしてから、単語内の母音の総数をtotal_values
変数に返します。 私たちのプログラムは、doctestを除いて、これに似ているはずです。
def count_vowels(word):
total_vowels = 0
for letter in word:
if letter in 'aeiou':
total_vowels += 1
return total_vowels
これらのトピックに関する詳細なガイダンスが必要な場合は、Pythonブックまたは補完的なシリーズでコーディングする方法を確認してください。
次に、プログラムの下部にmain
句を追加し、doctestモジュールをインポートして実行します。
if __name__ == "__main__":
import doctest
doctest.testmod()
この時点で、これが私たちのプログラムです:
def count_vowels(word):
"""
Given a single word, return the total number of vowels in that single word.
:param word: str
:return: int
>>> count_vowels('Cusco')
2
>>> count_vowels('Manila')
3
"""
total_vowels = 0
for letter in word:
if letter in 'aeiou':
total_vowels += 1
return total_vowels
if __name__ == "__main__":
import doctest
doctest.testmod()
python
(または仮想環境によってはpython3
)コマンドを使用してプログラムを実行できます。
- python counting_vowels.py
プログラムが上記と同じである場合、すべてのテストに合格しているはずであり、出力は表示されません。 これは、テストに合格したことを意味します。 このサイレント機能は、他の目的でプログラムを実行している場合に役立ちます。 特にテストのために実行している場合は、次のように-v
フラグを使用することをお勧めします。
- python counting_vowels.py -v
これを行うと、次の出力が表示されます。
OutputTrying:
count_vowels('Cusco')
Expecting:
2
ok
Trying:
count_vowels('Manila')
Expecting:
3
ok
1 items had no tests:
__main__
1 items passed all tests:
2 tests in __main__.count_vowels
2 tests in 2 items.
2 passed and 0 failed.
Test passed.
素晴らしい! テストに合格しました。 それでも、私たちのコードはまだすべてのエッジケースに対して完全に最適化されていない可能性があります。 doctestを使用してコードを強化する方法を学びましょう。
Doctestを使用してコードを改善する
この時点で、作業プログラムがあります。 まだ最高のプログラムではないかもしれないので、エッジケースを見つけてみましょう。 大文字の母音を追加するとどうなりますか?
doctestに別の例を追加します。今回は、トルコの都市で'Istanbul'
を試してみましょう。 マニラと同様に、イスタンブールにも3つの母音があります。
これが新しい例で更新されたプログラムです:
def count_vowels(word):
"""
Given a single word, return the total number of vowels in that single word.
:param word: str
:return: int
>>> count_vowels('Cusco')
2
>>> count_vowels('Manila')
3
>>> count_vowels('Istanbul')
3
"""
total_vowels = 0
for letter in word:
if letter in 'aeiou':
total_vowels += 1
return total_vowels
if __name__ == "__main__":
import doctest
doctest.testmod()
プログラムをもう一度実行してみましょう。
- python counting_vowels.py
エッジケースを特定しました! これは私たちが受け取った出力です:
Output**********************************************************************
File "counting_vowels.py", line 14, in __main__.count_vowels
Failed example:
count_vowels('Istanbul')
Expected:
3
Got:
2
**********************************************************************
1 items had failures:
1 of 3 in __main__.count_vowels
***Test Failed*** 1 failures.
上記の出力は、'Istanbul'
のテストが失敗したことを示しています。 プログラムに3つの母音がカウントされることを期待していると伝えましたが、代わりにプログラムは2つしかカウントしませんでした。 ここで何が悪かったのですか?
私たちの行if letter in 'aeiou':
では、小文字の母音のみを渡しました。 'aeiou'
文字列を'AEIOUaeiou'
に変更して、大文字と小文字の両方の母音をカウントするか、より洗練された方法で、word
に格納されている値を変換できます。 word.lower()
で小文字に変換します。 後者をやってみましょう。
def count_vowels(word):
"""
Given a single word, return the total number of vowels in that single word.
:param word: str
:return: int
>>> count_vowels('Cusco')
2
>>> count_vowels('Manila')
3
>>> count_vowels('Istanbul')
3
"""
total_vowels = 0
for letter in word.lower():
if letter in 'aeiou':
total_vowels += 1
return total_vowels
if __name__ == "__main__":
import doctest
doctest.testmod()
これで、プログラムを実行すると、すべてのテストに合格するはずです。 詳細フラグを指定してpython counting_vowels.py -v
を実行すると、再度確認できます。
それでも、これはおそらく最良のプログラムではなく、すべてのエッジケースを考慮しているわけではありません。
文字列値'Sydney'
(オーストラリアの都市の場合)をword
に渡すとどうなりますか? 3つの母音または1つの母音を期待しますか? 英語では、y
は母音と見なされることがあります。 さらに、値'Würzburg'
(ドイツの都市の場合)を使用すると、'ü'
はカウントされますか? それが必要ですか? 他の英語以外の単語をどのように処理しますか? UTF-16やUTF-32で使用可能な文字エンコードなど、さまざまな文字エンコードを使用する単語をどのように処理しますか?
ソフトウェア開発者は、サンプルプログラムで母音としてカウントする文字を決定するなど、難しい決定を行う必要がある場合があります。 正しい答えも間違った答えもない場合もあります。 多くの場合、可能性の全範囲を考慮することはありません。 したがって、doctest
モジュールは、考えられるエッジケースを検討し、予備的なドキュメントを取得するための優れたツールですが、最終的には、すべての人に役立つ堅牢なプログラムを構築するために、人間によるユーザーテスト(おそらくは共同作業者)が必要になります。
結論
このチュートリアルでは、doctest
モジュールを、ソフトウェアをテストおよび文書化する方法としてだけでなく、最初に文書化し、次にテストし、次にコードを記述することによって、プログラミングを開始する前に考える方法として紹介しました。
テストを書かないと、バグだけでなくソフトウェアの障害にもつながる可能性があります。 コードを書く前にテストを書く習慣を身につけることで、他の開発者やエンドユーザーに同様に役立つ生産的なソフトウェアをサポートできます。
テストとデバッグについて詳しく知りたい場合は、「Pythonプログラムのデバッグ」シリーズをご覧ください。 また、 Pythonでコーディングする方法に関する無料の電子書籍と、Python機械学習プロジェクトに関する別の電子書籍もあります。