Pytest の基本

完了

Pytest でテストを開始しましょう。 前のユニットで説明したように、Pytest は高度に構成可能であり、複雑なテスト スイートを処理できますが、テストの作成を開始する必要はあまりありません。 実際、フレームワークは簡単にテストを記述できるほど優れています。

このセクションの終わりまでに、初めてのテストの記述を開始し、Pytest でそれらを実行するために必要なものがすべて揃うはずです。

規約

テストの記述に取り組む前に、Pytest が依存するテスト規則の一部について説明する必要があります。

Python では、テスト ファイル、テスト ディレクトリ、または一般的なテスト レイアウトに関する多くのハード ルールはありません。 これらの規則を知ることで、追加の構成を必要とせずに、自動的なテストの検出と実行を利用できます。

テスト ディレクトリとテスト ファイル

テストのメイン ディレクトリは tests ディレクトリです。 このディレクトリはプロジェクトのルート レベルに配置できますが、これがコード モジュールと同じ場所にあるのを目にすることも珍しくありません。

このモジュールでは、既定でプロジェクトのルートで テスト を使用します。

jformat という名前の小さな Python プロジェクトのルートを見てみましょう。

.
├── README.md
├── jformat
│   ├── __init__.py
│   └── main.py
├── setup.py
└── tests
    └── test_main.py

tests ディレクトリはプロジェクトのルートにあり、1 つのテスト ファイルが含まれています。 このケースでは、テスト ファイルは test_main.py と呼ばれます。 この例は、以下の 2 つの重要な規則を示しています。

  • テスト ファイルと入れ子になったテスト ディレクトリを配置するために tests ディレクトリを使用します。
  • テスト ファイルの先頭に test を付けます。 プレフィックスは、ファイルにテスト コードが含まれていることを示します。

注意事項

ディレクトリ名として test (単数形) を使用しないでください。 test という名前は Python モジュールであるため、同じ名前のディレクトリを作成するとオーバーライドされます。 代わりに、常に複数形の tests を使用してください。

テスト Functions

Pytest を使用するための強力な引数は、テスト関数を記述できるということです。 テスト ファイルと同様に、テスト関数には test_ というプレフィックスを付ける必要があります。 test_ というプレフィックスは、Pytest がそのテストを収集して実行することを保証します。

単純なテスト関数は次のようになります。

def test_main():
    assert "a string value" == "a string value"

unittestに慣れている場合は、テスト関数でassertを使用するのは驚くかもしれません。 プレーンなアサートについては後で詳しく説明しますが、Pytest では、プレーンなアサートでリッチな失敗に関するレポートが得られます。

テスト クラスとテスト メソッド

ファイルと関数の規則と同様に、テスト クラスとテスト メソッドでは以下の規則を使用します。

  • テスト クラスの先頭に Test を付けます
  • テスト メソッドの先頭に test_ を付けます

Python の unittest ライブラリとの主な違いは、継承の必要がないことです。

次の例では、クラスとメソッドに対してこれらのプレフィックスとその他の Python の名前付け規則を使用します。 これは、アプリケーションでユーザー名を確認する小規模なテスト クラスを示しています。

class TestUser:

    def test_username(self):
        assert default() == "default username"

テストの実行

Pytest は、テスト フレームワークであり、テスト ランナーでもあります。 テスト ランナーは、コマンド ラインの実行可能ファイルであり、(大まかに言えば) 次のことが可能です。

  • テスト実行のためのすべてのテスト ファイル、テスト クラス、テスト関数を見つけることで、テストの収集を実行します。
  • すべてのテストを実行することで、テスト実行を開始します。
  • 失敗、エラー、合格のテストを追跡します。
  • テスト実行の最後にリッチなレポートを提供します。

Pytest は外部ライブラリであるため、それを使用するにはインストールする 必要があります

test_main.py ファイル内のこれらの内容を考えると、テストを実行する Pytest の動作を確認できます。

# contents of test_main.py file

def test_main():
    assert True

コマンド ラインにおいて、test_main.py ファイルが存在するのと同じパスで、実行可能ファイル pytest を実行できます。

 $ pytest
=========================== test session starts ============================
platform -- Python 3.10.1, pytest-6.2.5, py-1.11.0, pluggy-1.0.0
rootdir: /private/tmp/project
collected 1 item

test_main.py .                                                       [100%]

============================ 1 passed in 0.00s =============================

裏では、Pytest がどのような構成も必要とせずにテスト ファイル内のサンプル テストを収集します。

強力なアサート ステートメント

現状、ここでのテスト例はすべてプレーンな assert 呼び出しを使用しています。 通常、Python では、アサーションが失敗したときに適切なレポートがないため、 assert ステートメントはテストに使用されません。 しかし、Pytest にはこの制限はありません。 Pytest はバックグラウンドで、ユーザーにコードの記述や構成を強制することなく、ステートメントで豊富な比較を実行できるようにします。

プレーン assert ステートメントを使用すると、Python の演算子 ( ><!=>=<=など) を使用できます。 Python の演算子はすべて有効です。 この機能は Pytest の最も重要な機能の 1 つかもしれません。アサーションを記述するための新しい構文を学習する必要はありません。

Python オブジェクトとの一般的な比較を扱うときに、それがどのように変換されるかを見てみましょう。 ここでは、長い文字列を比較するときの失敗に関するレポートを確認してみましょう。

================================= FAILURES =================================
____________________________ test_long_strings _____________________________

    def test_long_strings():
        left = "this is a very long strings to be compared with another long string"
        right = "This is a very long string to be compared with another long string"
>       assert left == right
E       AssertionError: assert 'this is a ve...r long string' == 'This is a ve...r long string'
E         - This is a very long string to be compared with another long string
E         ? ^
E         + this is a very long strings to be compared with another long string
E         ? ^                         +

test_main.py:4: AssertionError

Pytest は、エラーに関する便利なコンテキストを示しています: 文字列の先頭における間違った大文字小文字の使用と、単語に余計な文字が含まれていること。 ただし、文字列以外のオブジェクトやデータ構造には Pytest が役立ちます。 たとえば、これがリストに関してどのように動作するかを次に示します。

________________________________ test_lists ________________________________

    def test_lists():
        left = ["sugar", "wheat", "coffee", "salt", "water", "milk"]
        right = ["sugar", "coffee", "wheat", "salt", "water", "milk"]
>       assert left == right
E       AssertionError: assert ['sugar', 'wh...ater', 'milk'] == ['sugar', 'co...ater', 'milk']
E         At index 1 diff: 'wheat' != 'coffee'
E         Full diff:
E         - ['sugar', 'coffee', 'wheat', 'salt', 'water', 'milk']
E         ?                     ---------
E         + ['sugar', 'wheat', 'coffee', 'salt', 'water', 'milk']
E         ?           +++++++++

test_main.py:9: AssertionError

このレポートは、インデックス 1 (リスト内の 2 番目の項目) が異なっていることを識別します。 これは、インデックス番号を特定するだけでなく、失敗の説明も提供します。 項目の比較とは別に、項目が見つからない場合に報告したり、どの項目が存在するかを正確に示す情報を提供したりすることもできます。 次のケースでは、それは "milk" です。

________________________________ test_lists ________________________________

    def test_lists():
        left = ["sugar", "wheat", "coffee", "salt", "water", "milk"]
        right = ["sugar", "wheat", "salt", "water", "milk"]
>       assert left == right
E       AssertionError: assert ['sugar', 'wh...ater', 'milk'] == ['sugar', 'wh...ater', 'milk']
E         At index 2 diff: 'coffee' != 'salt'
E         Left contains one more item: 'milk'
E         Full diff:
E         - ['sugar', 'wheat', 'salt', 'water', 'milk']
E         + ['sugar', 'wheat', 'coffee', 'salt', 'water', 'milk']
E         ?                    ++++++++++

test_main.py:9: AssertionError

最後に、ディクショナリでの動作を見てみましょう。 失敗が発生した場合の 2 つの大きなディクショナリの比較は膨大な作業になる可能性がありますが、Pytest は以下のようにコンテキストの提供と失敗の特定において優れた仕事を行います。

____________________________ test_dictionaries _____________________________

    def test_dictionaries():
        left = {"street": "Ferry Ln.", "number": 39, "state": "Nevada", "zipcode": 30877, "county": "Frett"}
        right = {"street": "Ferry Lane", "number": 38, "state": "Nevada", "zipcode": 30877, "county": "Frett"}
>       assert left == right
E       AssertionError: assert {'county': 'F...rry Ln.', ...} == {'county': 'F...ry Lane', ...}
E         Omitting 3 identical items, use -vv to show
E         Differing items:
E         {'street': 'Ferry Ln.'} != {'street': 'Ferry Lane'}
E         {'number': 39} != {'number': 38}
E         Full diff:
E           {
E            'county': 'Frett',...
E
E         ...Full output truncated (12 lines hidden), use '-vv' to show

このテストでは、ディクショナリに 2 つのエラーがあります。 1 つは "street" の値が異なり、もう 1 つは "number" が一致しないということです。

Pytest はこれらの違いを正確に検出します (1 回のテストで 1 つの失敗であっても)。 辞書には多数の項目が含まれているため、Pytest は同じ部分を省略し、関連するコンテンツのみを表示します。 提案された -vv フラグを使用して出力の詳細度を高めた場合の動作を見てみましょう。

____________________________ test_dictionaries _____________________________

    def test_dictionaries():
        left = {"street": "Ferry Ln.", "number": 39, "state": "Nevada", "zipcode": 30877, "county": "Frett"}
        right = {"street": "Ferry Lane", "number": 38, "state": "Nevada", "zipcode": 30877, "county": "Frett"}
>       assert left == right
E       AssertionError: assert {'county': 'Frett',\n 'number': 39,\n 'state': 'Nevada',\n 'street': 'Ferry Ln.',\n 'zipcode': 30877} == {'county': 'Frett',\n 'number': 38,\n 'state': 'Nevada',\n 'street': 'Ferry Lane',\n 'zipcode': 30877}
E         Common items:
E         {'county': 'Frett', 'state': 'Nevada', 'zipcode': 30877}
E         Differing items:
E         {'number': 39} != {'number': 38}
E         {'street': 'Ferry Ln.'} != {'street': 'Ferry Lane'}
E         Full diff:
E           {
E            'county': 'Frett',
E         -  'number': 38,
E         ?             ^
E         +  'number': 39,
E         ?             ^
E            'state': 'Nevada',
E         -  'street': 'Ferry Lane',
E         ?                    - ^
E         +  'street': 'Ferry Ln.',
E         ?                     ^
E            'zipcode': 30877,
E           }

pytest -vv を実行すると、詳細なレポートの量が増え、きめ細かい比較が行われます。 このレポートは、失敗を検出して表示するだけでなく、問題を修復するための変更をすばやく行うことも可能にします。