Pythonでのテストの開始
このチュートリアルは、Pythonですばらしいアプリケーションを作成したが、まだテストを作成していない人を対象としています。
Pythonでのテストは大きなトピックであり、非常に複雑になる可能性がありますが、難しくする必要はありません。 いくつかの簡単な手順でアプリケーションの簡単なテストの作成を開始し、そこから構築できます。
このチュートリアルでは、ユーザーが行う前に、基本的なテストを作成して実行し、バグを見つける方法を学びます! テストの作成と実行、アプリケーションのパフォーマンスの確認、さらにはセキュリティの問題の検索に使用できるツールについて学びます。
Free Bonus:5 Thoughts On Python Masteryは、Python開発者向けの無料コースで、Pythonスキルを次のレベルに引き上げるために必要なロードマップと考え方を示しています。
コードをテストする
コードをテストするには多くの方法があります。 このチュートリアルでは、最も基本的な手順からテクニックを学び、高度な方法を目指します。
自動化vs. 手動テスト
幸いなことに、気付かないうちに既にテストを作成している可能性があります。 アプリケーションを実行して初めて使用したときのことを覚えていますか? 機能を確認し、それらを使用して実験しましたか? これはexploratory testingと呼ばれ、手動テストの形式です。
探索的テストは、計画なしで行われるテストの一種です。 探索的テストでは、アプリケーションを探索するだけです。
手動テストの完全なセットを得るために必要なことは、アプリケーションが持つすべての機能、受け入れ可能なさまざまなタイプの入力、および期待される結果のリストを作成することだけです。 これで、コードに変更を加えるたびに、そのリストのすべての項目を調べて確認する必要があります。
それはあまり面白そうに聞こえませんか?
これが自動テストの出番です。 自動テストとは、人間ではなくスクリプトによるテスト計画(テストするアプリケーションの部分、テストする順序、予想される応答)の実行です。 Pythonには、アプリケーションの自動テストの作成に役立つツールとライブラリのセットが既に付属しています。 このチュートリアルでは、これらのツールとライブラリについて説明します。
単体テストと 統合テスト
テストの世界では用語の不足はありません。自動テストと手動テストの違いがわかったので、今度はさらに深いレベルに進んでください。
車のライトをテストする方法を考えてください。 ライト(test step)をオンにして車の外に出るか、友人にライトがオンになっていることを確認するように依頼します(test assertion)。 複数のコンポーネントのテストは、integration testingとして知られています。
簡単なタスクで正しい結果を得るために、正しく機能する必要があるすべてのことを考えてください。 これらのコンポーネントは、アプリケーションのパーツ、作成したすべてのクラス、関数、モジュールのようなものです。
統合テストの主な課題は、統合テストで正しい結果が得られない場合です。 システムのどの部分に障害が発生しているかを特定することなく、問題を診断することは非常に困難です。 ライトが点灯しない場合は、電球が壊れている可能性があります。 バッテリーは切れていますか? オルタネーターはどうですか? 車のコンピューターが故障していますか?
あなたが派手な近代的な車を持っているなら、それはあなたの電球が消えたときにあなたを教えてくれます。 これは、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 "", line 1, in
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")
これで、test case、アサーション、およびエントリポイント(コマンドライン)を記述しました。 これをコマンドラインで実行できます:
$ python test_sum.py
Everything passed
成功した結果、Everything passed
を確認できます。
Pythonでは、sum()
は最初の引数としてiterableを受け入れます。 リストでテストしました。 タプルでもテストします。 次のコードを使用して、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
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つのテストを実行しました。
Note: Python2と3の両方で実行する必要があるテストケースを作成する場合は注意してください。 Python 2.7以下では、unittest
はunittest2
と呼ばれます。 unittest
からインポートするだけで、Python2と3の間で機能が異なるさまざまなバージョンを取得できます。
unittest
の詳細については、unittest Documentationを参照してください。
nose
時間の経過とともに、アプリケーションに対して数百または数千ものテストを作成すると、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
は、実行するテストをフィルタリングするための多くのコマンドラインフラグを提供します。 詳細については、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
、クラスの使用、およびコマンドラインエントリポイントを削除しました。
詳細については、Pytest Documentation Websiteを参照してください。
最初のテストを書く
これまでに学んだことをまとめて、組み込みの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
テストを追加するにつれて、単一のファイルが乱雑になり、保守が困難になることがわかります。そのため、tests/
というフォルダーを作成して、テストを複数のファイルに分割できます。 すべてのテストランナーがPythonファイルに実行するテストが含まれていると想定するように、各ファイルがtest_
で始まるようにするのが慣例です。 一部の非常に大規模なプロジェクトでは、テストを目的または使用法に基づいてより多くのサブディレクトリに分割しています。
Note:アプリケーションが単一のスクリプトである場合はどうなりますか?
組み込みの__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()
は次のようになります。-
変数
data
を数値のリスト(1, 2, 3)
で宣言します -
my_sum.sum(data)
の結果をresult
変数に割り当てます -
unittest.TestCase
クラスで.assertEqual()
メソッドを使用して、result
の値が6
に等しいことを表明します。
-
-
unittest
テストランナー.main()
を実行するコマンドラインエントリポイントを定義します
self
が何であるか、または.assertEqual()
がどのように定義されているかわからない場合は、Python 3 Object-Oriented Programmingを使用してオブジェクト指向プログラミングをブラッシュアップできます。
アサーションの書き方
テストを記述する最後のステップは、既知の応答に対して出力を検証することです。 これはassertionとして知られています。 アサーションの記述方法に関する一般的なベストプラクティスがいくつかあります。
-
テストが繰り返し可能であることを確認し、テストを複数回実行して、毎回同じ結果が得られるようにします
-
結果が
sum()
の例の値の実際の合計であることを確認するなど、入力データに関連する結果をアサートしてみてください
unittest
には、値、型、および変数の存在をアサートするための多くのメソッドが付属しています。 最も一般的に使用される方法の一部を次に示します。
方法 | に相当 |
---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.assertIs()
、.assertIsNone()
、.assertIn()
、および.assertIsInstance()
にはすべて、.assertIsNot()
などの名前の反対のメソッドがあります。
副作用
テストを書くとき、それは多くの場合、関数の戻り値を見るほど簡単ではありません。 多くの場合、コードの一部を実行すると、クラスの属性、ファイルシステム上のファイル、データベース内の値など、環境内の他のものが変更されます。 これらはside effectsとして知られており、テストの重要な部分です。 アサーションのリストに副作用を含める前に、副作用をテストするかどうかを決定します。
テストしたいコードの単位に多くの副作用があることがわかった場合は、Single Responsibility Principleを壊している可能性があります。 単一の責任原則を破るということは、コードの部分があまりにも多くのことをしているので、リファクタリングする方が良いということです。 単一責任原則に従うことは、コードを設計するための優れた方法であり、最終的には信頼性の高いアプリケーション向けの反復可能で単純な単体テストを簡単に記述できます。
最初のテストの実行
最初のテストを作成したので、それを実行します。 もちろん、合格することはわかっていますが、より複雑なテストを作成する前に、テストを正常に実行できることを確認する必要があります。
テストランナーの実行
テストコードを実行し、アサーションをチェックし、コンソールでテスト結果を提供するPythonアプリケーションは、test runnerと呼ばれます。
test.py
の下部に、次の小さなコードスニペットを追加しました。
if __name__ == '__main__':
unittest.main()
これはコマンドラインエントリポイントです。 これは、コマンドラインでpython test.py
を実行してスクリプトを単独で実行すると、unittest.main()
が呼び出されることを意味します。 これは、unittest.TestCase
から継承するこのファイル内のすべてのクラスを検出することにより、テストランナーを実行します。
これは、unittest
テストランナーを実行する多くの方法の1つです。 test.py
という名前の単一のテストファイルがある場合、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)
)を含むアサーションの詳細
-
python -m unittest
コマンドに-v
フラグを追加することで、テスト出力に追加情報を追加できることを忘れないでください。
PyCharmからテストを実行する
PyCharm IDEを使用している場合は、次の手順に従ってunittest
またはpytest
を実行できます。
-
プロジェクトツールウィンドウで、
tests
ディレクトリを選択します。 -
コンテキストメニューで、
unittest
の実行コマンドを選択します。 たとえば、Run ‘Unittests in my Tests…’を選択します。
これにより、テストウィンドウでunittest
が実行され、PyCharm内で結果が得られます。
詳細については、PyCharm Websiteを参照してください。
Visual Studioコードからテストを実行する
Microsoft Visual Studio Code IDEを使用している場合、unittest
、nose
、およびpytest
の実行のサポートがPythonプラグインに組み込まれています。
Pythonプラグインがインストールされている場合は、Ctrl[.kbd .key-shift]##Shift##[.kbd .key-p]#P#を指定してコマンドパレットを開き、「Python test」と入力して、テストの構成を設定できます。 さまざまなオプションが表示されます。
Debug All Unit Testsを選択すると、VSCodeはテストフレームワークを構成するためのプロンプトを表示します。 歯車をクリックして、テストランナー(unittest
)とホームディレクトリ(.
)を選択します。
これが設定されると、ウィンドウの下部にテストのステータスが表示されます。テストログにすばやくアクセスし、これらのアイコンをクリックしてテストを再実行できます。
これは、テストが実行されていることを示していますが、一部のテストは失敗しています。
DjangoやFlaskなどのWebフレームワークのテスト
DjangoやFlaskなどの一般的なフレームワークの1つを使用してWebアプリケーションのテストを作成する場合、テストの作成と実行の方法にいくつかの重要な違いがあります。
他のアプリケーションと異なる理由
Webアプリケーションでテストするすべてのコードを考えてください。 ルート、ビュー、およびモデルはすべて、使用されるフレームワークに関する多くのインポートと知識を必要とします。
これは、チュートリアルの最初にある車のテストに似ています。ライトのチェックなどの簡単なテストを実行する前に、車のコンピューターを起動する必要があります。
DjangoとFlaskはどちらも、unittest
に基づくテストフレームワークを提供することで、これを簡単にします。 学習してきた方法でテストを書き続けることができますが、実行方法は少し異なります。
Djangoテストランナーの使用方法
Djangostartapp
テンプレートは、アプリケーションディレクトリ内にtests.py
ファイルを作成します。 まだお持ちでない場合は、次の内容で作成できます:
from django.test import TestCase
class MyTestCase(TestCase):
# Your test methods
これまでの例との主な違いは、unittest.TestCase
ではなくdjango.test.TestCase
から継承する必要があることです。 これらのクラスのAPIは同じですが、DjangoTestCase
クラスは、テストに必要なすべての状態を設定します。
テストスイートを実行するには、コマンドラインでunittest
を使用する代わりに、manage.py test
を使用します。
$ python manage.py test
複数のテストファイルが必要な場合は、tests.py
をtests
というフォルダーに置き換え、__init__.py
という名前の空のファイルを挿入して、test_*.py
ファイルを作成します。 Djangoはこれらを発見して実行します。
詳細については、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
コマンドを使用してテストケースを実行できます。
詳細については、Flask Documentation Websiteを参照してください。
より高度なテストシナリオ
アプリケーションのテストを作成する前に、各テストの3つの基本的なステップを覚えておいてください。
-
入力を作成する
-
コードを実行し、出力をキャプチャします
-
出力を期待される結果と比較します
文字列や数字のような入力の静的な値を作成するのと同じくらい簡単ではありません。 アプリケーションがクラスまたはコンテキストのインスタンスを必要とする場合があります。 だったらどうしようか?
入力として作成するデータは、fixtureと呼ばれます。 フィクスチャを作成して再利用するのが一般的な方法です。
同じテストを実行し、毎回異なる値を渡し、同じ結果を期待している場合、これはparameterizationと呼ばれます。
予想される障害の処理
以前、sum()
をテストするシナリオのリストを作成したときに、質問が出てきました。単一の整数や文字列などの不正な値を指定するとどうなりますか?
この場合、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つのテストがアプリケーションの状態に影響を与えて別のテストが失敗する可能性があるため、副作用によりユニットテストが難しくなります。
多くの副作用があるアプリケーションの部分をテストするために使用できるいくつかの簡単な手法があります。
-
単一責任の原則に従うようにコードをリファクタリングする
-
副作用を取り除くためのメソッドまたは関数呼び出しのモックアウト
-
アプリケーションのこの部分に対して単体テストの代わりに統合テストを使用する
モックに慣れていない場合は、いくつかの優れた例について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
と呼ばれる無料のパッケージがあり、応答フィクスチャを作成してテストフォルダに保存する方法を提供します。 その他のon their GitHub Pageをご覧ください。
複数の環境でのテスト
これまで、特定の依存関係のセットを持つ仮想環境を使用して、単一バージョンの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
ファイルがアプリケーションフォルダーにある必要があります。 持っていない場合は、続行する前に、setup.py
を作成する方法についてthis guideに従うことができます。
または、プロジェクトが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
依存関係が変更された場合、またはsite-packagesが破損した場合は、仮想環境を再作成します。
$ tox -r
より冗長な出力でToxを実行します。
$ tox -q
より詳細な出力でToxを実行する:
$ tox -v
Toxの詳細については、Tox Documentation Websiteを参照してください。
テストの実行を自動化する
これまで、コマンドを実行して手動でテストを実行してきました。 変更を加えて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つはリンターと呼ばれます。 リンターがコードを見てコメントします。 あなたが犯した間違いについてのヒントを提供し、末尾のスペースを修正し、導入したバグを予測することさえできます。
リンターの詳細については、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を使用することにした場合は、flake8
構成セクションをtox.ini
内に配置できます。
この例では、.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
構成オプションの完全なリストは、Documentation Websiteで入手できます。
これで、CI構成にflake8
を追加できます。 Travis CIの場合、これは次のようになります。
matrix:
include:
- python: "2.7"
script: "flake8"
Travisは.flake8
で構成を読み取り、リンティングエラーが発生した場合はビルドに失敗します。 必ずflake8
依存関係をrequirements.txt
ファイルに追加してください。
コードフォーマッターを使用した積極的なリンティング
flake8
はパッシブリンターです。変更をお勧めしますが、コードを変更する必要があります。 より積極的なアプローチは、コードフォーマッタです。 コードフォーマッタは、スタイルとレイアウトのプラクティスのコレクションに合わせてコードを自動的に変更します。
black
は非常に容赦のないフォーマッターです。 設定オプションはなく、非常に具体的なスタイルがあります。 これにより、テストパイプラインに追加するドロップインツールとして最適です。
Note:black
にはPython3.6以降が必要です。
pipを介してblack
をインストールできます。
$ pip install black
次に、コマンドラインでblack
を実行するには、フォーマットするファイルまたはディレクトリを指定します。
$ black test.py
テストコードをクリーンに保つ
テストを作成するとき、通常のアプリケーションで行うよりも多くのコードをコピーして貼り付けることに気付くかもしれません。 テストは非常に繰り返し行われる場合がありますが、それは決してコードをずさんで保守が難しいままにする理由ではありません。
時間の経過とともに、テストコードで多くのtechnical debtが開発されます。アプリケーションに大幅な変更があり、テストの変更が必要な場合は、構造化の方法が原因で、必要以上に面倒な作業になる可能性があります。それら。
テストを作成するときは、DRYの原則に従うようにしてください:Don’tRepeatYourself。
テストフィクスチャと関数は、保守しやすいテストコードを生成するための優れた方法です。 また、読みやすさも重要です。 テストコード上に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
をテストランナーとして使用することにした場合の別のオプションは、pytest-benchmark
プラグインです。 これにより、benchmark
と呼ばれるpytest
フィクスチャが提供されます。 benchmark()
に任意の呼び出し可能オブジェクトを渡すことができ、呼び出し可能オブジェクトのタイミングをpytest
の結果に記録します。
pip
を使用してPyPIからpytest-benchmark
をインストールできます。
$ pip install pytest-benchmark
次に、フィクスチャを使用し、実行する呼び出し可能オブジェクトを渡すテストを追加できます。
def test_my_function(benchmark):
result = benchmark(test)
pytest
を実行すると、ベンチマーク結果が得られます。
詳細については、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
詳細については、GitHub Websiteを参照してください。
結論
Pythonは、アプリケーションが設計どおりに動作することを検証するために必要なコマンドとライブラリを組み込むことにより、テストをアクセス可能にしました。 Pythonでのテストの開始は、複雑である必要はありません。unittest
を使用して、コードを検証するための小さくて保守可能なメソッドを作成できます。
テストの詳細を学び、アプリケーションが成長するにつれて、pytest
などの他のテストフレームワークの1つに切り替えることを検討し、より高度な機能の活用を開始できます。
読んでくれてありがとう。 Pythonのバグのない未来を願っています!