Pythonでのテストの開始
このチュートリアルは、Pythonですばらしいアプリケーションを作成したが、まだテストを作成していない人を対象としています。
Pythonでのテストは大きなトピックであり、非常に複雑になる可能性がありますが、難しくする必要はありません。 いくつかの簡単な手順でアプリケーションの簡単なテストの作成を開始し、そこから構築できます。
このチュートリアルでは、ユーザーが行う前に、基本的なテストを作成して実行し、バグを見つける方法を学びます! テストの作成と実行、アプリケーションのパフォーマンスの確認、さらにはセキュリティの問題の検索に使用できるツールについて学びます。
無料ボーナス: link:[5 Thoughts On Python Mastery]、Python開発者向けの無料コースで、Pythonのスキルを次のレベルに引き上げるのに必要なロードマップと考え方を示します。
コードをテストする
コードをテストするには多くの方法があります。 このチュートリアルでは、最も基本的な手順からテクニックを学び、高度な方法を目指します。
自動化vs. 手動テスト
幸いなことに、気付かないうちに既にテストを作成している可能性があります。 アプリケーションを実行して初めて使用したときのことを覚えていますか? 機能を確認し、それらを使用して実験しましたか? これは「探索的テスト」として知られ、手動テストの一種です。
探索的テストは、計画なしで行われるテストの一種です。 探索的テストでは、アプリケーションを探索するだけです。
手動テストの完全なセットを得るために必要なことは、アプリケーションが持つすべての機能、受け入れ可能なさまざまなタイプの入力、および期待される結果のリストを作成することだけです。 これで、コードに変更を加えるたびに、そのリストのすべての項目を調べて確認する必要があります。
それはあまり面白そうに聞こえませんか?
これが自動テストの出番です。 自動テストとは、人間ではなくスクリプトによるテスト計画(テストするアプリケーションの部分、テストする順序、予想される応答)の実行です。 Pythonには、アプリケーションの自動テストの作成に役立つツールとライブラリのセットが既に付属しています。 このチュートリアルでは、これらのツールとライブラリについて説明します。
単体テストと 統合テスト
テストの世界では用語の不足はありません。自動テストと手動テストの違いがわかったので、今度はさらに深いレベルに進んでください。
車のライトをテストする方法を考えてください。 ライト(テストステップ)をオンにして車の外に出るか、友人にライトが点灯していることを確認するよう依頼します(テストアサーション)。 複数のコンポーネントのテストは、*統合テスト*として知られています。
簡単なタスクで正しい結果を得るために、正しく機能する必要があるすべてのことを考えてください。 これらのコンポーネントは、アプリケーションのパーツ、作成したすべてのクラス、関数、モジュールのようなものです。
統合テストの主な課題は、統合テストで正しい結果が得られない場合です。 システムのどの部分に障害が発生しているかを特定することなく、問題を診断することは非常に困難です。 ライトが点灯しない場合は、電球が壊れている可能性があります。 バッテリーは切れていますか? オルタネーターはどうですか? 車のコンピューターが故障していますか?
あなたが派手な近代的な車を持っているなら、それはあなたの電球が消えたときにあなたを教えてくれます。 unit test の形式を使用してこれを行います。
単体テストは小規模なテストで、単一のコンポーネントが適切に動作することを確認します。 単体テストを使用すると、アプリケーションの何が壊れているかを特定し、より迅速に修正できます。
次の2種類のテストを見ました。
-
統合テストは、アプリケーション内のコンポーネントが相互に動作することを確認します。
-
単体テストは、アプリケーションの小さなコンポーネントをチェックします。
統合テストと単体テストの両方をPythonで作成できます。 組み込み関数 `+ sum()`の単体テストを作成するには、既知の出力に対して ` sum()+`の出力をチェックします。
たとえば、数字の「(1、2、3)」の「+ sum()」が「+6」に等しいことを確認する方法は次のとおりです。
>>>
>>> assert sum([1, 2, 3]) == 6, "Should be 6"
値が正しいため、REPLには何も出力されません。
`+ sum()`の結果が正しくない場合、これは ` AssertionError `とメッセージ `" Should be 6 "`で失敗します。 間違った値でアサーションステートメントを再試行して、 ` AssertionError +`を確認します。
>>>
>>> assert sum([1, 1, 1]) == 6, "Should be 6"
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AssertionError: Should be 6
REPLでは、 `+ sum()`の結果が ` 6 `と一致しないため、発生した ` AssertionError +`が表示されます。
REPLでテストする代わりに、これを `+ test_sum.py +`という新しいPythonファイルに入れて、再度実行する必要があります。
def test_sum():
assert sum([1, 2, 3]) == 6, "Should be 6"
if __name__ == "__main__":
test_sum()
print("Everything passed")
これで、テストケース、アサーション、およびエントリポイント(コマンドライン)を作成しました。 これをコマンドラインで実行できます:
$ python test_sum.py
Everything passed
成功した結果、「+ Everything Passed +」を確認できます。
Pythonでは、 `+ sum()`は最初の引数として反復可能なものを受け入れます。 リストでテストしました。 タプルでもテストします。 次のコードを使用して、 ` test_sum_2.py +`という新しいファイルを作成します。
def test_sum():
assert sum([1, 2, 3]) == 6, "Should be 6"
def test_sum_tuple():
assert sum((1, 2, 2)) == 6, "Should be 6"
if __name__ == "__main__":
test_sum()
test_sum_tuple()
print("Everything passed")
`+ test_sum_2.py `を実行すると、 `(1、2、2)`の ` sum()`は ` 6 `ではなく ` 5 +`であるため、スクリプトはエラーを返します。 。 スクリプトの結果は、エラーメッセージ、コード行、およびトレースバックを提供します。
$ python test_sum_2.py
Traceback (most recent call last):
File "test_sum_2.py", line 9, in <module>
test_sum_tuple()
File "test_sum_2.py", line 5, in test_sum_tuple
assert sum((1, 2, 2)) == 6, "Should be 6"
AssertionError: Should be 6
ここでは、コードの間違いがコンソール上でエラーをどのように与えているか、エラーの場所と予想される結果についての情報を確認できます。
この方法でテストを書くことは簡単なチェックでは問題ありませんが、複数のテストが失敗した場合はどうなりますか? これがテストランナーの出番です。 テストランナーは、テストの実行、出力の確認、およびテストとアプリケーションのデバッグと診断のためのツールを提供するために設計された特別なアプリケーションです。
テストランナーの選択
Pythonには多くのテストランナーがあります。 Python標準ライブラリに組み込まれているものは、「+ unittest 」と呼ばれます。 このチュートリアルでは、 ` unittest `テストケースと ` unittest `テストランナーを使用します。 「 unittest +」の原則は、他のフレームワークに簡単に移植できます。 最も人気のある3つのテストランナーは次のとおりです。
-
+ unittest +
-
+ nose +`または `+ nose2 +
-
+ pytest +
要件と経験レベルに最適なテストランナーを選択することが重要です。
+ unittest +
`+ unittest +`は、バージョン2.1以降、Python標準ライブラリに組み込まれています。 おそらく市販のPythonアプリケーションやオープンソースプロジェクトで見るでしょう。
`+ unittest `には、テストフレームワークとテストランナーの両方が含まれています。 ` unittest +`には、テストの作成と実行に関するいくつかの重要な要件があります。
`+ unittest +`には以下が必要です:
-
テストをメソッドとしてクラスに入れます
-
組み込みの `+ assert `ステートメントの代わりに、 ` unittest.TestCase +`クラスで一連の特別なアサーションメソッドを使用します
前の例を `+ unittest +`テストケースに変換するには、以下を行う必要があります。
-
標準ライブラリから `+ unittest +`をインポートします
-
`+ TestCase `クラスから継承する ` TestSum +`というクラスを作成します
-
最初の引数として「+ self +」を追加して、テスト関数をメソッドに変換します
-
アサーションを変更して、 `+ TestCase `クラスで ` self.assertEqual()+`メソッドを使用します
-
コマンドラインエントリポイントを変更して、 `+ unittest.main()+`を呼び出します
次のコードで新しいファイル `+ test_sum_unittest.py +`を作成して、これらの手順に従います。
import unittest
class TestSum(unittest.TestCase):
def test_sum(self):
self.assertEqual(sum([1, 2, 3]), 6, "Should be 6")
def test_sum_tuple(self):
self.assertEqual(sum((1, 2, 2)), 6, "Should be 6")
if __name__ == '__main__':
unittest.main()
コマンドラインでこれを実行すると、1つの成功( `。`で示される)と1つの失敗( `+ F +`で示される)が表示されます。
$ python test_sum_unittest.py
.F
======================================================================
FAIL: test_sum_tuple (__main__.TestSum)
----------------------------------------------------------------------
Traceback (most recent call last):
File "test_sum_unittest.py", line 9, in test_sum_tuple
self.assertEqual(sum((1, 2, 2)), 6, "Should be 6")
AssertionError: Should be 6
----------------------------------------------------------------------
Ran 2 tests in 0.001s
FAILED (failures=1)
`+ unittest +`テストランナーを使用して2つのテストを実行しました。
*注: *Python 2と3の両方で実行する必要があるテストケースを作成する場合は注意してください。 Python 2.7以前では、「+ unittest 」は「 unittest2 」と呼ばれます。 ` unittest +`から単にインポートする場合、Python 2と3の間で異なる機能を備えた異なるバージョンを取得します。
`+ unittest +`の詳細については、https://docs.python.org/3/library/unittest.html [unittest Documentation]を参照してください。
鼻
時間が経つにつれて、アプリケーション用に数百または数千ものテストを記述すると、 `+ unittest +`からの出力を理解して使用することがますます難しくなることがあります。
`+ nose `は ` unittest `フレームワークを使用して記述されたテストと互換性があり、 ` unittest `テストランナーのドロップイン置換として使用できます。 オープンソースアプリケーションとしての「 nose 」の開発は遅れ、「 nose2 」と呼ばれるフォークが作成されました。 最初から始める場合は、「 nose 」ではなく「 nose2 +」を使用することをお勧めします。
`+ nose2 `を開始するには、PyPIから ` nose2 `をインストールし、コマンドラインで実行します。 ` nose2 `は、 ` test* .py `という名前のすべてのテストスクリプトと、現在のディレクトリの ` unittest.TestCase +`を継承するテストケースを検出しようとします。
$ pip install nose2
$ python -m nose2
.F
======================================================================
FAIL: test_sum_tuple (__main__.TestSum)
----------------------------------------------------------------------
Traceback (most recent call last):
File "test_sum_unittest.py", line 9, in test_sum_tuple
self.assertEqual(sum((1, 2, 2)), 6, "Should be 6")
AssertionError: Should be 6
----------------------------------------------------------------------
Ran 2 tests in 0.001s
FAILED (failures=1)
`+ nose2 `テストランナーから ` test_sum_unittest.py `で作成したテストを実行しました。 ` nose2 +`は、実行するテストをフィルタリングするための多くのコマンドラインフラグを提供します。 詳細については、https://nose2.readthedocs.io/[Nose 2 documentation]をご覧ください。
+ pytest +
`+ pytest `は ` unittest `テストケースの実行をサポートします。 ` pytest `の本当の利点は、 ` pytest `テストケースを書くことです。 ` pytest `テストケースは、 ` test_ +`という名前で始まるPythonファイル内の一連の関数です。
`+ pytest +`には他にもいくつかの素晴らしい機能があります:
*特別な `+ self.assert* ()+`メソッドを使用する代わりに、組み込みの `+ assert +`ステートメントをサポート * テストケースのフィルタリングのサポート * 最後に失敗したテストから再実行する機能 * 機能を拡張する数百のプラグインのエコシステム
`+ pytest `の ` TestSum +`テストケースの例を記述すると、次のようになります。
def test_sum():
assert sum([1, 2, 3]) == 6, "Should be 6"
def test_sum_tuple():
assert sum((1, 2, 2)) == 6, "Should be 6"
+ TestCase +
、クラスの使用、およびコマンドラインエントリポイントを削除しました。
詳細については、https://docs.pytest.org/en/latest/[Pytest Documentation Webサイト]をご覧ください。
最初のテストを書く
これまでに学んだことをまとめて、組み込みの `+ sum()+`関数をテストする代わりに、同じ要件の単純な実装をテストします。
新しいプロジェクトフォルダーを作成し、その中に「+ my_sum 」という新しいフォルダーを作成します。 ` my_sum `内で、 ` init 。py `という空のファイルを作成します。 ` init 。py `ファイルを作成すると、親ディレクトリからモジュールとして ` my_sum +`フォルダーをインポートできます。
プロジェクトフォルダは次のようになります。
project/ │ └── my_sum/ └── __init__.py
`+ my_sum/ init 。py `を開いて、 ` sum()+`と呼ばれる新しい関数を作成します。この関数は反復可能(リスト、タプル、またはセット)を取り、値を一緒に追加します。
def sum(arg):
total = 0
for val in arg:
total += val
return total
このコード例では、「+ total 」という変数を作成し、「 arg 」のすべての値を反復処理して、「 total +」に追加します。 次に、イテラブルが使い果たされると結果を返します。
テストを書く場所
テストの作成を開始するには、最初のテストケースを含む `+ test.py `というファイルを作成するだけです。 ファイルをテストするにはアプリケーションをインポートできる必要があるため、パッケージフォルダーの上に ` test.py +`を配置すると、ディレクトリツリーは次のようになります。
project/ │ ├── my_sum/ │ └── __init__.py | └── test.py
テストを追加すると、1つのファイルが乱雑になり、保守が困難になるため、「+ tests/」というフォルダーを作成して、テストを複数のファイルに分割できます。 すべてのテストランナーが実行するテストがPythonファイルに含まれていると想定するように、各ファイルが「 test_ +」で始まることを確認するのが慣例です。 一部の非常に大規模なプロジェクトでは、テストを目的または使用法に基づいてより多くのサブディレクトリに分割しています。
*注意:*アプリケーションが単一のスクリプトである場合はどうなりますか?
組み込みの `+ import ()`関数を使用して、クラス、関数、変数など、スクリプトの任意の属性をインポートできます。 ` from my_sum import sum +`の代わりに、次のように書くことができます:
target = __import__("my_sum.py")
sum = target.sum
`+ import ()`を使用する利点は、プロジェクトフォルダーをパッケージに変換する必要がなく、ファイル名を指定できることです。 これは、ファイル名が標準ライブラリパッケージと競合する場合にも役立ちます。 たとえば、 ` math.py `は ` math +`モジュールと衝突します。
簡単なテストを構成する方法
テストを作成する前に、まずいくつかの決定を行います。
-
何をテストしますか?
-
単体テストまたは統合テストを書いていますか?
次に、テストの構造はこのワークフローに大まかに従う必要があります。
-
入力を作成する
-
テスト対象のコードを実行し、出力をキャプチャします
-
出力を期待される結果と比較します
このアプリケーションでは、 `+ sum()`をテストしています。 ` sum()+`には、次のような多くの動作を確認できます:
-
整数(整数)のリストを合計できますか?
-
タプルまたはセットを合計できますか?
-
フロートのリストを合計できますか?
-
単一の整数や文字列などの不適切な値を指定するとどうなりますか?
-
値の1つが負の場合はどうなりますか?
最も簡単なテストは、整数のリストです。 次のPythonコードでファイル「+ test.py +」を作成します。
import unittest
from my_sum import sum
class TestSum(unittest.TestCase):
def test_list_int(self):
"""
Test that it can sum a list of integers
"""
data = [1, 2, 3]
result = sum(data)
self.assertEqual(result, 6)
if __name__ == '__main__':
unittest.main()
このコード例:
-
作成した `+ my_sum `パッケージから ` sum()+`をインポートします
-
`+ unittest.TestCase `を継承する ` TestSum +`という新しいテストケースクラスを定義します。
-
整数のリストをテストするためのテストメソッド `+ .test_list_int()`を定義します。 メソッド ` .test_list_int()+`は以下を行います:
-
数値のリスト `(1、2、3)`で変数 `+ data +`を宣言します
-
`+ my_sum.sum(data)`の結果を ` result +`変数に割り当てる
-
`+ unittest.TestCase `クラスで ` .assertEqual()`メソッドを使用して、 ` result `の値が ` 6 +`に等しいことをアサートします
-
-
コマンドラインのエントリポイントを定義し、 `+ unittest `テストランナー ` .main()+`を実行します
`+ self `の定義や ` .assertEqual()+`の定義が不明な場合は、https://realpython.com/python3-object-oriented-programmingでオブジェクト指向プログラミングをブラッシュアップできます。/[Python 3オブジェクト指向プログラミング]。
アサーションの書き方
テストを記述する最後のステップは、既知の応答に対して出力を検証することです。 これは*アサーション*と呼ばれます。 アサーションの記述方法に関する一般的なベストプラクティスがいくつかあります。
-
テストが繰り返し可能であることを確認し、テストを複数回実行して、毎回同じ結果が得られるようにします
-
結果が `+ sum()+`の例の値の実際の合計であることを確認するなど、入力データに関連する結果を試してアサートします
`+ unittest +`には、変数の値、型、および存在をアサートする多くのメソッドが付属しています。 最も一般的に使用される方法の一部を次に示します。
Method | Equivalent to |
---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
+ .assertIs()+
、 + .assertIsNone()+
、 + .assertIn()+
、および `+ .assertIsInstance()`にはすべて、 ` .assertIsNot()+`という名前の反対のメソッドがあります、など。
副作用
テストを書くとき、それは多くの場合、関数の戻り値を見るほど簡単ではありません。 多くの場合、コードの一部を実行すると、クラスの属性、ファイルシステム上のファイル、データベース内の値など、環境内の他のものが変更されます。 これらは*副作用*と呼ばれ、テストの重要な部分です。 アサーションのリストに副作用を含める前に、副作用をテストするかどうかを決定します。
テストするコードのユニットに多くの副作用があることがわかった場合は、https://en.wikipedia.org/wiki/Single_responsibility_principle [単一責任の原則]に違反している可能性があります。 単一の責任原則を破るということは、コードの部分があまりにも多くのことをしているので、リファクタリングする方が良いということです。 単一責任原則に従うことは、コードを設計するための優れた方法であり、最終的には信頼性の高いアプリケーション向けの反復可能で単純な単体テストを簡単に記述できます。
最初のテストの実行
最初のテストを作成したので、それを実行します。 もちろん、合格することはわかっていますが、より複雑なテストを作成する前に、テストを正常に実行できることを確認する必要があります。
テストランナーの実行
テストコードを実行し、アサーションをチェックし、コンソールでテスト結果を提供するPythonアプリケーションは、*テストランナー*と呼ばれます。
`+ test.py +`の下部に、次の小さなコードスニペットを追加しました。
if __name__ == '__main__':
unittest.main()
これはコマンドラインエントリポイントです。 つまり、コマンドラインで `+ python test.py `を実行してスクリプトを単独で実行すると、 ` unittest.main()`が呼び出されます。 これは、このファイル内で ` unittest.TestCase +`を継承するすべてのクラスを検出することにより、テストランナーを実行します。
これは、 `+ unittest `テストランナーを実行する多くの方法の1つです。 ` test.py `という名前のテストファイルが1つしかない場合、 ` python test.py +`を呼び出すのがよい方法です。
別の方法は、 `+ unittest +`コマンドラインを使用することです。 これを試して:
$ python -m unittest test
これにより、コマンドライン経由で同じテストモジュール(「+ test +」と呼ばれる)が実行されます。
出力を変更するための追加オプションを提供できます。 それらの1つは、詳細を表す `+ -v +`です。 次に試してください:
$ python -m unittest -v test
test_list_int (test.TestSum) ... ok
----------------------------------------------------------------------
Ran 1 tests in 0.000s
これにより、 `+ test.py +`内で1つのテストが実行され、結果がコンソールに出力されました。 詳細モードでは、最初に実行したテストの名前と各テストの結果がリストされました。
テストを含むモジュールの名前を提供する代わりに、次を使用して自動検出を要求できます。
$ python -m unittest discover
これは、 `+ test *.py +`という名前のファイルを現在のディレクトリで検索し、テストを試みます。
複数のテストファイルを作成したら、 `+ test* .py `命名パターンに従っている限り、 ` -s +`フラグとディレクトリ名を使用して代わりにディレクトリの名前を指定できます。
$ python -m unittest discover -s tests
`+ unittest +`は、単一のテスト計画ですべてのテストを実行し、結果を提供します。
最後に、ソースコードがディレクトリルートになく、サブディレクトリ、たとえば「+ src/」というフォルダに含まれている場合、モジュールをインポートできるように、テストを実行する場所を「 unittest 」に伝えることができます` -t +`フラグで正しく:
$ python -m unittest discover -s tests -t src
`+ unittest `は ` src/`ディレクトリに変更され、 ` tests `ディレクトリ内のすべての ` test * .py +`ファイルをスキャンして実行します。
テスト出力について
これは非常に単純な例で、すべてが成功するので、今度は失敗したテストを試みて出力を解釈します。
`+ sum()+`は、分数のような数値型の他のリストを受け入れることができるはずです。
`+ test.py `ファイルの先頭に、標準ライブラリの ` fractions `モジュールから ` Fraction +`タイプをインポートするためのインポートステートメントを追加します。
from fractions import Fraction
次に、誤った値を期待するアサーションでテストを追加します。この場合、1/4、1/4、および2/5の合計が1であると想定しています。
import unittest
from my_sum import sum
class TestSum(unittest.TestCase):
def test_list_int(self):
"""
Test that it can sum a list of integers
"""
data = [1, 2, 3]
result = sum(data)
self.assertEqual(result, 6)
def test_list_fraction(self):
"""
Test that it can sum a list of fractions
"""
data = [Fraction(1, 4), Fraction(1, 4), Fraction(2, 5)]
result = sum(data)
self.assertEqual(result, 1)
if __name__ == '__main__':
unittest.main()
`+ python -m unittest test +`でテストを再度実行すると、次の出力が表示されます。
$ python -m unittest test
F.
======================================================================
FAIL: test_list_fraction (test.TestSum)
----------------------------------------------------------------------
Traceback (most recent call last):
File "test.py", line 21, in test_list_fraction
self.assertEqual(result, 1)
AssertionError: Fraction(9, 10) != 1
----------------------------------------------------------------------
Ran 2 tests in 0.001s
FAILED (failures=1)
出力には、次の情報が表示されます。
-
最初の行は、すべてのテストの実行結果を示しています。1つは失敗(
+ F +
)、もう1つは合格(。
)です。 -
`+ FAIL +`エントリには、失敗したテストに関する詳細が表示されます。
-
テストメソッド名(
+ test_list_fraction +
) -
テストモジュール(
+ test +
)とテストケース(+ TestSum +
) -
失敗した行へのトレースバック *期待される結果(
+ 1 +
)と実際の結果(+ Fraction(9、10)+
)を含むアサーションの詳細
-
`+ -v `フラグを ` python -m unittest +`コマンドに追加することで、テスト出力に追加情報を追加できることを忘れないでください。
PyCharmからテストを実行する
PyCharm IDEを使用している場合、次の手順に従って `+ unittest `または ` pytest +`を実行できます。
-
プロジェクトツールウィンドウで、「+ tests +」ディレクトリを選択します。
-
コンテキストメニューで、 `+ unittest +`の実行コマンドを選択します。 たとえば、「テストのユニットテスト…」を実行します。
これにより、テストウィンドウで `+ unittest +`が実行され、PyCharm内で結果が表示されます。
詳細については、https://www.jetbrains.com/help/pycharm/performing-tests.html [PyCharm Webサイト]をご覧ください。
Visual Studioコードからテストを実行する
Microsoft Visual Studio Code IDEを使用している場合、 + unittest +
、 + nose +
、および `+ pytest +`実行のサポートがPythonプラグインに組み込まれています。
Pythonプラグインがインストールされている場合は、[。keys]#Ctrl + Shift + P#でコマンドパレットを開き、「Python test」と入力して、テストの構成をセットアップできます。 さまざまなオプションが表示されます。
_Debug All Unit Tests_を選択すると、VSCodeはテストフレームワークを構成するためのプロンプトを表示します。 歯車をクリックして、テストランナー( + unittest +
)とホームディレクトリ( 。
)を選択します。
これが設定されると、ウィンドウの下部にテストのステータスが表示されます。テストログにすばやくアクセスし、これらのアイコンをクリックしてテストを再実行できます。
これは、テストが実行されていることを示していますが、一部のテストは失敗しています。
DjangoやFlaskなどのWebフレームワークのテスト
DjangoやFlaskなどの一般的なフレームワークの1つを使用してWebアプリケーションのテストを作成する場合、テストの作成と実行の方法にいくつかの重要な違いがあります。
他のアプリケーションと異なる理由
Webアプリケーションでテストするすべてのコードを考えてください。 ルート、ビュー、およびモデルはすべて、使用されるフレームワークに関する多くのインポートと知識を必要とします。
これは、チュートリアルの最初にある車のテストに似ています。ライトのチェックなどの簡単なテストを実行する前に、車のコンピューターを起動する必要があります。
DjangoとFlaskはどちらも、 `+ unittest +`に基づいたテストフレームワークを提供することで、これを簡単にします。 学習してきた方法でテストを書き続けることができますが、実行方法は少し異なります。
Djangoテストランナーの使用方法
Djangoの `+ startapp `テンプレートは、アプリケーションディレクトリ内に ` tests.py +`ファイルを作成します。 まだお持ちでない場合は、次の内容で作成できます:
from django.test import TestCase
class MyTestCase(TestCase):
# Your test methods
これまでの例との大きな違いは、 `+ unittest.TestCase `の代わりに ` django.test.TestCase `から継承する必要があることです。 これらのクラスは同じAPIを持っていますが、Djangoの ` TestCase +`クラスはテストに必要なすべての状態を設定します。
コマンドラインで `+ unittest `を使用する代わりにテストスイートを実行するには、 ` manage.py test +`を使用します:
$ python manage.py test
複数のテストファイルが必要な場合は、「+ tests.py 」を「 tests 」というフォルダーに置き換え、「 init 。py 」という空のファイルを挿入して、「 test _* 。py +」ファイルを作成します。 Djangoはこれらを発見して実行します。
詳細については、https://docs.djangoproject.com/en/2.1/topics/testing/overview/[Django Documentation Website]をご覧ください。
`+ unittest +`とFlaskの使用方法
Flaskでは、アプリをインポートしてからテストモードに設定する必要があります。 テストクライアントをインスタンス化し、テストクライアントを使用して、アプリケーション内の任意のルートにリクエストを行うことができます。
テストクライアントのインスタンス化はすべて、テストケースの `+ setUp `メソッドで行われます。 次の例では、 ` my_app `がアプリケーションの名前です。 「 setUp +」の機能がわからなくても心配しないでください。 これについては、リンク:#more-advanced-testing-scenarios [その他の高度なテストシナリオ]セクションで学習します。
テストファイル内のコードは次のようになります。
import my_app
import unittest
class MyTestCase(unittest.TestCase):
def setUp(self):
my_app.app.testing = True
self.app = my_app.app.test_client()
def test_home(self):
result = self.app.get('/')
# Make your assertions
その後、 `+ python -m unittest discover +`コマンドを使用してテストケースを実行できます。
詳細については、http://flask.pocoo.org/docs/0.12/testing/[Flask Documentation Website]で入手できます。
より高度なテストシナリオ
アプリケーションのテストを作成する前に、各テストの3つの基本的なステップを覚えておいてください。
-
入力を作成する
-
コードを実行し、出力をキャプチャします
-
出力を期待される結果と比較します
文字列や数字のような入力の静的な値を作成するのと同じくらい簡単ではありません。 アプリケーションがクラスまたはコンテキストのインスタンスを必要とする場合があります。 だったらどうしようか?
入力として作成するデータは、「フィクスチャー」と呼ばれます。 フィクスチャを作成して再利用するのが一般的な方法です。
同じテストを実行し、毎回異なる値を渡し、同じ結果を期待している場合、これは*パラメーター化*と呼ばれます。
予想される障害の処理
以前、 `+ sum()+`をテストするシナリオのリストを作成したときに、1つの整数や文字列などの悪い値を指定するとどうなりますか?
この場合、 `+ sum()+`がエラーをスローすることを期待します。 エラーがスローされると、テストが失敗します。
予想されるエラーを処理する特別な方法があります。 `+ .assertRaises()`をコンテキストマネージャーとして使用し、 ` with +`ブロック内でテストステップを実行できます。
import unittest
from my_sum import sum
class TestSum(unittest.TestCase):
def test_list_int(self):
"""
Test that it can sum a list of integers
"""
data = [1, 2, 3]
result = sum(data)
self.assertEqual(result, 6)
def test_list_fraction(self):
"""
Test that it can sum a list of fractions
"""
data = [Fraction(1, 4), Fraction(1, 4), Fraction(2, 5)]
result = sum(data)
self.assertEqual(result, 1)
def test_bad_type(self):
data = "banana"
with self.assertRaises(TypeError):
result = sum(data)
if __name__ == '__main__':
unittest.main()
このテストケースは、 `+ sum(data)`が ` TypeError `を発生させた場合にのみ合格します。 ` TypeError +`を選択した例外タイプに置き換えることができます。
アプリケーションの動作を分離する
チュートリアルの前半で、副作用とは何かを学びました。 テストを実行するたびに異なる結果が得られたり、さらに悪いことに、1つのテストがアプリケーションの状態に影響を与えて別のテストが失敗する可能性があるため、副作用によりユニットテストが難しくなります。
多くの副作用があるアプリケーションの部分をテストするために使用できるいくつかの簡単な手法があります。
-
単一責任の原則に従うようにコードをリファクタリングする
-
副作用を取り除くためのメソッドまたは関数呼び出しのモックアウト
-
アプリケーションのこの部分に対して単体テストの代わりに統合テストを使用する
モックに慣れていない場合は、https://realpython.com/python-cli-testing/#mocks [Python CLI Testing]をご覧ください。
統合テストの作成
これまで、主に単体テストについて学習してきました。 単体テストは、予測可能で安定したコードを構築するための優れた方法です。 しかし、結局のところ、アプリケーションは起動時に動作する必要があります!
統合テストとは、アプリケーションの複数のコンポーネントをテストして、それらが連携して動作することを確認することです。 統合テストでは、アプリケーションの消費者またはユーザーのように行動する必要があります。
-
HTTP REST APIを呼び出す
-
Python APIを呼び出す
-
Webサービスを呼び出す
-
コマンドラインを実行する
これらの各タイプの統合テストは、入力、実行、およびアサートパターンに従って、単体テストと同じ方法で作成できます。 最も重要な違いは、統合テストが一度により多くのコンポーネントをチェックしているため、単体テストよりも多くの副作用があることです。 また、統合テストでは、データベース、ネットワークソケット、構成ファイルなど、より多くのフィクスチャを配置する必要があります。
これが、ユニットテストと統合テストを分けるのが良い習慣である理由です。 テストデータベースやテストケースなどの統合に必要なフィクスチャの作成は、ユニットテストよりも実行に時間がかかることが多いため、コミットごとに1回ではなく、本番環境にプッシュする前に統合テストを実行することをお勧めします。
単体テストと統合テストを分離する簡単な方法は、単純に異なるフォルダーに配置することです。
project/ │ ├── my_app/ │ └── __init__.py │ └── tests/ | ├── unit/ | ├── __init__.py | └── test_sum.py | └── integration/ ├── __init__.py └── test_integration.py
テストの選択グループのみを実行する多くの方法があります。 ソースディレクトリの指定フラグである `+ -s `は、テストを含むパスで ` unittest discover +`に追加できます。
$ python -m unittest discover -s tests/integration
`+ unittest `は、 ` tests/integration +`ディレクトリ内のすべてのテストの結果を提供します。
データ駆動型アプリケーションのテスト
多くの統合テストでは、データベースなどのバックエンドデータが特定の値で存在する必要があります。 たとえば、データベース内の100人を超える顧客に対してアプリケーションが正しく表示されるか、製品名が日本語で表示されていても注文ページが機能するかどうかを確認するテストが必要な場合があります。
これらのタイプの統合テストは、異なるテストフィクスチャに依存して、再現性と予測可能性を確認します。
使用する良い方法は、テストデータが含まれていることを示すために、テストデータを「+ fixtures +」と呼ばれる統合テストフォルダー内のフォルダーに保存することです。 次に、テスト内で、データをロードしてテストを実行できます。
データがJSONファイルで構成されている場合の構造の例を次に示します。
project/ │ ├── my_app/ │ └── __init__.py │ └── tests/ | └── unit/ | ├── __init__.py | └── test_sum.py | └── integration/ | ├── fixtures/ | ├── test_basic.json | └── test_complex.json | ├── __init__.py └── test_integration.py
テストケース内で、 `+ .setUp()`メソッドを使用して、既知のパスのフィクスチャファイルからテストデータをロードし、そのテストデータに対して多くのテストを実行できます。 1つのPythonファイルに複数のテストケースを含めることができ、 ` unittest +`ディスカバリーが両方を実行することに注意してください。 テストデータのセットごとに1つのテストケースを作成できます。
import unittest
class TestBasic(unittest.TestCase):
def setUp(self):
# Load test data
self.app = App(database='fixtures/test_basic.json')
def test_customer_count(self):
self.assertEqual(len(self.app.customers), 100)
def test_existence_of_customer(self):
customer = self.app.get_customer(id=10)
self.assertEqual(customer.name, "Org XYZ")
self.assertEqual(customer.address, "10 Red Road, Reading")
class TestComplexData(unittest.TestCase):
def setUp(self):
# load test data
self.app = App(database='fixtures/test_complex.json')
def test_customer_count(self):
self.assertEqual(len(self.app.customers), 10000)
def test_existence_of_customer(self):
customer = self.app.get_customer(id=9999)
self.assertEqual(customer.name, u"バナナ")
self.assertEqual(customer.address, "10 Red Road, Akihabara, Tokyo")
if __name__ == '__main__':
unittest.main()
アプリケーションがリモートAPIなどのリモートロケーションからのデータに依存している場合は、テストが再現可能であることを確認する必要があります。 APIがオフラインであるか、接続の問題があるためにテストが失敗すると、開発が遅くなる可能性があります。 このような状況では、リモートフィクスチャをローカルに保存して、それらを呼び出してアプリケーションに送信できるようにすることをお勧めします。
`+ requests `ライブラリには、 ` responses +`と呼ばれる無料のパッケージがあり、応答フィクスチャを作成してテストフォルダに保存する方法を提供します。 GitHubページをご覧ください。
複数の環境でのテスト
これまで、特定の依存関係のセットを持つ仮想環境を使用して、単一バージョンのPythonに対してテストを行ってきました。 アプリケーションがPythonの複数のバージョン、またはパッケージの複数のバージョンで動作することを確認したい場合があります。 Toxは、複数の環境でのテストを自動化するアプリケーションです。
Toxのインストール
Toxは、 `+ pip +`を介してインストールするパッケージとしてPyPIで利用可能です:
$ pip install tox
Toxがインストールされたので、構成する必要があります。
依存関係のToxの構成
Toxは、プロジェクトディレクトリの構成ファイルを介して構成されます。 Tox構成ファイルには以下が含まれています。
-
テストを実行するために実行するコマンド
-
実行する前に必要な追加パッケージ
-
テスト対象のPythonバージョン
Tox構成構文を学習する代わりに、クイックスタートアプリケーションを実行することにより、有利なスタートを切ることができます。
$ tox-quickstart
Tox設定ツールはそれらの質問をし、 `+ tox.ini +`に次のようなファイルを作成します:
[tox]
envlist = py27, py36
[testenv]
deps =
commands =
python -m unittest discover
Toxを実行する前に、パッケージをインストールする手順を含むアプリケーションフォルダーに `+ setup.py `ファイルが必要です。 持っていない場合は、続行する前にhttps://packaging.python.org/tutorials/packaging-projects/#setup-py [このガイド]に従って ` setup.py +`を作成する方法を確認できます。
あるいは、プロジェクトがPyPIでの配布用ではない場合、 `+ [tox] `見出しの下の ` tox.ini +`ファイルに次の行を追加することにより、この要件をスキップできます。
[tox]
envlist = py27, py36
skipsdist=True
`+ setup.py `を作成せず、アプリケーションにPyPIからの依存関係がある場合、 ` [testenv] +`セクションの下の複数の行でそれらを指定する必要があります。 たとえば、Djangoには次のものが必要です。
[testenv]
deps = django
その段階を完了すると、テストを実行する準備が整います。
これでToxを実行でき、Python 2.7用とPython 3.6用の2つの仮想環境が作成されます。 Toxディレクトリは `+ .tox/`と呼ばれます。 ` .tox/`ディレクトリ内で、Toxは各仮想環境に対して ` python -m unittest discover +`を実行します。
このプロセスを実行するには、コマンドラインでToxを呼び出します。
$ tox
Toxは、各環境に対するテストの結果を出力します。 Toxを初めて実行すると、仮想環境を作成するのに少し時間がかかりますが、一度実行すると、2回目の実行ははるかに高速になります。
Toxの実行
Toxの出力は非常に簡単です。 各バージョンの環境を作成し、依存関係をインストールしてから、テストコマンドを実行します。
覚えておくと便利なコマンドラインオプションがいくつかあります。
Python 3.6などの単一の環境のみを実行します。
$ tox -e py36
依存関係が変更された場合、またはhttps://docs.python.org/3/install/#how-installation-works[site-packages]が破損した場合に、仮想環境を再作成します。
$ tox -r
より冗長な出力でToxを実行します。
$ tox -q
より詳細な出力でToxを実行する:
$ tox -v
Toxの詳細については、https://tox.readthedocs.io/en/latest/[Tox Documentation Webサイト]を参照してください。
テストの実行を自動化する
これまで、コマンドを実行して手動でテストを実行してきました。 変更を加えてGitなどのソース管理リポジトリにコミットするときにテストを自動的に実行するツールがいくつかあります。 自動テストツールは、CI/CDツールと呼ばれることが多く、「継続的統合/継続的展開」の略です。テストを実行し、アプリケーションをコンパイルして公開し、さらに運用環境に展開することもできます。
Travis CIは、利用可能な多くの利用可能なCI(継続的統合)サービスの1つです。
Travis CIはPythonとうまく機能し、これらのテストをすべて作成したので、クラウドでのテストの実行を自動化できます! Travis CIは、GitHubおよびGitLabのオープンソースプロジェクトには無料で、プライベートプロジェクトには無料で利用できます。
開始するには、Webサイトにログインし、GitHubまたはGitLabの資格情報で認証します。 次に、次の内容の `+ .travis.yml +`というファイルを作成します。
language: python
python:
- "2.7"
- "3.7"
install:
- pip install -r requirements.txt
script:
- python -m unittest discover
この構成は、Travis CIに以下を指示します。
-
Python 2.7および3.7に対してテストします(これらのバージョンを任意のバージョンに置き換えることができます。)
-
`+ requirements.txt +`にリストしたすべてのパッケージをインストールします(依存関係がない場合は、このセクションを削除する必要があります)。
-
`+ python -m unittest discover +`を実行してテストを実行します
このファイルをコミットしてプッシュすると、Travis CIはリモートGitリポジトリにプッシュするたびにこれらのコマンドを実行します。 あなたは彼らのウェブサイトで結果をチェックアウトできます。
次は何ですか
テストの作成、実行、プロジェクトへの組み込み、さらには自動実行の方法を学習したので、テストライブラリが大きくなると便利な高度なテクニックがいくつかあります。
アプリケーションへのリンターの導入
ToxおよびTravis CIには、テストコマンドの構成があります。 このチュートリアルを通して使用してきたテストコマンドは、「+ python -m unittest discover +」です。
これらすべてのツールで1つまたは複数のコマンドを提供できます。このオプションは、アプリケーションの品質を向上させるツールをさらに追加できるようにするためのものです。
そのようなタイプのアプリケーションの1つはリンターと呼ばれます。 リンターがコードを見てコメントします。 あなたが犯した間違いについてのヒントを提供し、末尾のスペースを修正し、導入したバグを予測することさえできます。
リンターの詳細については、https://realpython.com/python-code-quality/[Python Code Quality tutorial]をご覧ください。
`+ flake8 +`によるパッシブリンティング
PEP 8仕様に関連してコードのスタイルについてコメントする人気のリンターは、「+ flake8 +」です。
`+ pip `を使用して ` flake8 +`をインストールできます。
$ pip install flake8
その後、単一のファイル、フォルダー、またはパターンに対して `+ flake8 +`を実行できます。
$ flake8 test.py
test.py:6:1: E302 expected 2 blank lines, found 1
test.py:23:1: E305 expected 2 blank lines after class or function definition, found 1
test.py:24:20: W292 no newline at end of file
`+ flake8 +`が見つけたコードのエラーと警告のリストが表示されます。
`+ flake8 `はコマンドラインまたはプロジェクトの設定ファイル内で設定可能です。 上記の「 E305 」のような特定のルールを無視したい場合は、設定で設定できます。 ` flake8 `はプロジェクトフォルダー内の ` .flake8 `ファイルまたは ` setup.cfg `ファイルを検査します。 Toxを使用することにした場合、 ` tox.ini `内に ` flake8 +`設定セクションを配置できます。
この例では、「。git +」および「 pycache 」ディレクトリと「 E305 +」ルールを無視します。 また、最大行長を80文字ではなく90に設定します。 長いメソッド名、テスト値を含む文字列リテラル、およびより長くなる可能性のある他のデータが含まれているため、行幅のデフォルトの79文字の制約はテストで非常に制限されていることに気付くでしょう。 テストの行の長さを最大120文字に設定するのが一般的です。
[flake8]
ignore = E305
exclude = .git,__pycache__
max-line-length = 90
または、コマンドラインで次のオプションを提供できます。
$ flake8 --ignore E305 --exclude .git,__pycache__ --max-line-length=90
設定オプションの完全なリストは、http://flake8.pycqa.org/en/latest/user/options.html [Documentation Website]で入手できます。
CI構成に「+ flake8 +」を追加できるようになりました。 Travis CIの場合、これは次のようになります。
matrix:
include:
- python: "2.7"
script: "flake8"
Travisは `+ .flake8 `の設定を読み取り、リンティングエラーが発生するとビルドを失敗します。 必ず ` flake8 `依存関係を ` requirements.txt +`ファイルに追加してください。
コードフォーマッターを使用した積極的なリンティング
`+ flake8 +`は受動的なリンターです。変更を推奨しますが、コードを変更する必要があります。 より積極的なアプローチは、コードフォーマッタです。 コードフォーマッタは、スタイルとレイアウトのプラクティスのコレクションに合わせてコードを自動的に変更します。
`+ black +`は非常に容赦のないフォーマッタです。 設定オプションはなく、非常に具体的なスタイルがあります。 これにより、テストパイプラインに追加するドロップインツールとして最適です。
注意: `+ black +`にはPython 3.6+が必要です。
pipで `+ black +`をインストールできます:
$ pip install black
次に、コマンドラインで「+ black +」を実行するには、フォーマットするファイルまたはディレクトリを指定します。
$ black test.py
テストコードをクリーンに保つ
テストを作成するとき、通常のアプリケーションで行うよりも多くのコードをコピーして貼り付けることに気付くかもしれません。 テストは非常に繰り返し行われる場合がありますが、それは決してコードをずさんで保守が難しいままにする理由ではありません。
時間が経つにつれて、多くのhttps://martinfowler.com/bliki/TechnicalDebt.html[technical Debt]をテストコードで開発し、テストの変更を必要とする大幅な変更がアプリケーションにある場合は、あなたがそれらを構造化した方法のために必要以上に面倒なタスク。
テストを作成するときは、 DRY の原則に従うようにしてください。
テストフィクスチャと関数は、保守しやすいテストコードを生成するための優れた方法です。 また、読みやすさも重要です。 テストコード上に `+ flake8 +`のようなリンティングツールをデプロイすることを検討してください。
$ flake8 --max-line-length=120 tests/
変更間のパフォーマンス低下のテスト
Pythonでコードをベンチマークする方法は多数あります。 標準ライブラリは `+ timeit `モジュールを提供します。これは、関数を何度も時間計測し、配布することができます。 この例では、 ` test()`を100回実行し、 ` print()+`を出力します:
def test():
# ... your code
if __name__ == '__main__':
import timeit
print(timeit.timeit("test()", setup="from __main__ import test", number=100))
テストランナーとして `+ pytest `を使用することにした場合のもう1つのオプションは、 ` pytest-benchmark `プラグインです。 これにより、「 benchmark 」と呼ばれる「 pytest 」フィクスチャが提供されます。 任意の呼び出し可能オブジェクトに ` benchmark()`を渡すことができ、呼び出し可能オブジェクトのタイミングを ` pytest +`の結果に記録します。
`+ pip `を使用してPyPIから ` pytest-benchmark +`をインストールできます。
$ pip install pytest-benchmark
次に、フィクスチャを使用し、実行する呼び出し可能オブジェクトを渡すテストを追加できます。
def test_my_function(benchmark):
result = benchmark(test)
`+ pytest +`を実行すると、ベンチマーク結果が得られます:
詳細については、https://pytest-benchmark.readthedocs.io/en/latest/[Documentation Website]をご覧ください。
アプリケーションのセキュリティ欠陥のテスト
アプリケーションで実行するもう1つのテストは、一般的なセキュリティの間違いや脆弱性のチェックです。
`+ pip `を使用してPyPIから ` bandit +`をインストールできます。
$ pip install bandit
次に、 `+ -r +`フラグを使用してアプリケーションモジュールの名前を渡すと、要約が表示されます。
$ bandit -r my_sum
[main] INFO profile include tests: None
[main] INFO profile exclude tests: None
[main] INFO cli include tests: None
[main] INFO cli exclude tests: None
[main] INFO running on Python 3.5.2
Run started:2018-10-08 00:35:02.669550
Test results:
No issues identified.
Code scanned:
Total lines of code: 5
Total lines skipped (#nosec): 0
Run metrics:
Total issues (by severity):
Undefined: 0.0
Low: 0.0
Medium: 0.0
High: 0.0
Total issues (by confidence):
Undefined: 0.0
Low: 0.0
Medium: 0.0
High: 0.0
Files skipped (0):
`+ flake8 `と同様に、 ` bandit `フラグのルールは設定可能です。無視したいものがある場合は、オプションを使用して ` setup.cfg +`ファイルに次のセクションを追加できます。
[bandit]
exclude:/test
tests: B101,B102,B301
詳細については、https://github.com/PyCQA/bandit [GitHub Webサイト]をご覧ください。
結論
Pythonは、アプリケーションが設計どおりに動作することを検証するために必要なコマンドとライブラリを組み込むことにより、テストをアクセス可能にしました。 Pythonでのテストの開始は複雑である必要はありません。`+ unittest + `を使用して、小さく保守可能なメソッドを記述してコードを検証できます。
テストの詳細を学び、アプリケーションが成長するにつれて、 `+ pytest +`のような他のテストフレームワークのいずれかに切り替えることを検討し、より高度な機能を活用し始めることができます。
読んでくれてありがとう。 Pythonのバグのない未来を願っています!