Pythonでのテストの開始

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種類のテストを見ました。

  1. 統合テストは、アプリケーション内のコンポーネントが相互に動作することを確認します。

  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テストケースに変換するには、次のことを行う必要があります。

  1. 標準ライブラリからunittestをインポートします

  2. TestCaseクラスから継承するTestSumというクラスを作成します

  3. 最初の引数としてselfを追加して、テスト関数をメソッドに変換します

  4. TestCaseクラスでself.assertEqual()メソッドを使用するようにアサーションを変更します

  5. コマンドラインエントリポイントを変更して、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以下では、unittestunittest2と呼ばれます。 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ステートメントをサポート

  • テストケースのフィルタリングのサポート

  • 最後に失敗したテストから再実行する機能

  • 機能を拡張する数百のプラグインのエコシステム

pytestTestSumテストケースの例を書くと、次のようになります。

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.pymathモジュールと衝突します。

簡単なテストを構成する方法

テストを作成する前に、まずいくつかの決定を行います。

  1. 何をテストしますか?

  2. 単体テストまたは統合テストを書いていますか?

次に、テストの構造はこのワークフローに大まかに従う必要があります。

  1. 入力を作成する

  2. テスト対象のコードを実行し、出力をキャプチャします

  3. 出力を期待される結果と比較します

このアプリケーションでは、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()

このコード例:

  1. 作成したmy_sumパッケージからsum()をインポートします

  2. unittest.TestCaseから継承するTestSumという新しいテストケースクラスを定義します

  3. 整数のリストをテストするためのテストメソッド.test_list_int()を定義します。 メソッド.test_list_int()は次のようになります。

    • 変数dataを数値のリスト(1, 2, 3)で宣言します

    • my_sum.sum(data)の結果をresult変数に割り当てます

    • unittest.TestCaseクラスで.assertEqual()メソッドを使用して、resultの値が6に等しいことを表明します。

  4. unittestテストランナー.main()を実行するコマンドラインエントリポイントを定義します

selfが何であるか、または.assertEqual()がどのように定義されているかわからない場合は、Python 3 Object-Oriented Programmingを使用してオブジェクト指向プログラミングをブラッシュアップできます。

アサーションの書き方

テストを記述する最後のステップは、既知の応答に対して出力を検証することです。 これはassertionとして知られています。 アサーションの記述方法に関する一般的なベストプラクティスがいくつかあります。

  • テストが繰り返し可能であることを確認し、テストを複数回実行して、毎回同じ結果が得られるようにします

  • 結果がsum()の例の値の実際の合計であることを確認するなど、入力データに関連する結果をアサートしてみてください

unittestには、値、型、および変数の存在をアサートするための多くのメソッドが付属しています。 最も一般的に使用される方法の一部を次に示します。

方法 に相当

.assertEqual(a, b)

a == b

.assertTrue(x)

bool(x) is True

.assertFalse(x)

bool(x) is False

.assertIs(a, b)

a is b

.assertIsNone(x)

x is None

.assertIn(a, b)

a in b

.assertIsInstance(a, b)

isinstance(a, b)

.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

unittestsrc/ディレクトリに移動し、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. 最初の行は、すべてのテストの実行結果を示しています。1つは失敗し(F)、もう1つは合格しました(.)。

  2. FAILエントリには、失敗したテストに関する詳細が表示されます。

    • テストメソッド名(test_list_fraction

    • テストモジュール(test)とテストケース(TestSum

    • 失敗した行へのトレースバック

    • 期待される結果(1)と実際の結果(Fraction(9, 10))を含むアサーションの詳細

python -m unittestコマンドに-vフラグを追加することで、テスト出力に追加情報を追加できることを忘れないでください。

PyCharmからテストを実行する

PyCharm IDEを使用している場合は、次の手順に従ってunittestまたはpytestを実行できます。

  1. プロジェクトツールウィンドウで、testsディレクトリを選択します。

  2. コンテキストメニューで、unittestの実行コマンドを選択します。 たとえば、Run ‘Unittests in my Tests…’を選択します。

これにより、テストウィンドウでunittestが実行され、PyCharm内で結果が得られます。

PyCharm Testing

詳細については、PyCharm Websiteを参照してください。

Visual Studioコードからテストを実行する

Microsoft Visual Studio Code IDEを使用している場合、unittestnose、およびpytestの実行のサポートがPythonプラグインに組み込まれています。

Pythonプラグインがインストールされている場合は、Ctrl[.kbd .key-shift]##Shift##[.kbd .key-p]#P#を指定してコマンドパレットを開き、「Python test」と入力して、テストの構成を設定できます。 さまざまなオプションが表示されます。

Visual Studio Code Step 1

Debug All Unit Testsを選択すると、VSCodeはテストフレームワークを構成するためのプロンプトを表示します。 歯車をクリックして、テストランナー(unittest)とホームディレクトリ(.)を選択します。

これが設定されると、ウィンドウの下部にテストのステータスが表示されます。テストログにすばやくアクセスし、これらのアイコンをクリックしてテストを再実行できます。

Visual Studio Code Step 2

これは、テストが実行されていることを示していますが、一部のテストは失敗しています。

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.pytestsというフォルダーに置き換え、__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つの基本的なステップを覚えておいてください。

  1. 入力を作成する

  2. コードを実行し、出力をキャプチャします

  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つのテストがアプリケーションの状態に影響を与えて別のテストが失敗する可能性があるため、副作用によりユニットテストが難しくなります。

Testing Side Effects

多くの副作用があるアプリケーションの部分をテストするために使用できるいくつかの簡単な手法があります。

  • 単一責任の原則に従うようにコードをリファクタリングする

  • 副作用を取り除くためのメソッドまたは関数呼び出しのモックアウト

  • アプリケーションのこの部分に対して単体テストの代わりに統合テストを使用する

モックに慣れていない場合は、いくつかの優れた例について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を介してインストールするパッケージとしてP​​yPIで利用できます。

$ 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に以下を指示します。

  1. Python 2.7および3.7に対してテストします(これらのバージョンを任意のバージョンに置き換えることができます。)

  2. requirements.txtにリストするすべてのパッケージをインストールします(依存関係がない場合は、このセクションを削除する必要があります)。

  3. 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を実行すると、ベンチマーク結果が得られます。

Pytest benchmark screenshot

詳細については、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のバグのない未来を願っています!