演習
この演習では、 pytest と @pytest.mark.parametrize を使用して、例外を想定するケースを含む関数をテストします。 次に、xUnit-style setup_method() と teardown_method() アプローチを使用した、クラスベースのテストをフィクスチャにリファクタリングします。 パラメーター化とフィクスチャを使用すると、繰り返しが少なく、より明確なセットアップでより多くのケースをカバーするテストを記述するのに役立ちます。
セットアップのヘルプについては、仮想環境のPythonドキュメント、pip installの pip ドキュメント、および pytest のインストールの pytest ドキュメントを参照してください。 この演習の pytest 機能の詳細については、 parametrizing テスト関数、 予期される例外に関するアサーション、 フィクスチャ、 xUnit スタイルのセットアップ、 および一時ディレクトリとファイルに関する pytest ドキュメントを参照してください。
開始する前に
モジュールの前提条件で説明されているように、Python 3.10 以降を使用します。 pytest がインストールされ、アクティブ化された仮想環境が既にある場合は、手順 1 に進みます。 それ以外の場合は、 test_advanced.pyを作成するフォルダー内に仮想環境を作成してアクティブ化します。
Windows PowerShell の場合:
python -m venv .venv
.\.venv\Scripts\Activate.ps1
python -m pip install --upgrade pip
python -m pip install pytest
python -m pytest --version
macOS、Linux、またはLinux 用 Windows サブシステムの場合:
python3 -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
python -m pip install pytest
python -m pytest --version
version コマンドは、次の出力と同様に pytest バージョンを出力する必要があります。
pytest x.y.z
インストール コマンドでは、pytest は特定のバージョンにピン留めされません。 Pip では、パッケージ メタデータを使用して、仮想環境のPython バージョンと互換性のある安定した pytest リリースを選択します。
pytest がアクティブな環境から実行されるように、この演習全体で python -m pytest を使用します。 Windows PowerShell で Activate.ps1 がブロックされている場合は、PowerShell 実行ポリシー オプションの Python venv ドキュメントを参照するか、pip コマンドと pytest コマンドに対して仮想環境のPythonを直接実行します。 例えば次が挙げられます。
.\.venv\Scripts\python -m pip install --upgrade pip
.\.venv\Scripts\python -m pip install pytest
.\.venv\Scripts\python -m pytest -v test_advanced.py
ステップ 1 - この演習用のテスト ファイルを追加する
pytest を実行するのと同じフォルダーに、 test_advanced.pyという名前の新しいテスト ファイルを作成します。 .venv フォルダー内にファイルを配置しないでください。 次のコードを追加します。
import pytest def str_to_bool(string): normalized = string.lower() if normalized in ["yes", "y", "1"]: return True if normalized in ["no", "n", "0"]: return False raise ValueError(f"Cannot convert {string!r} to a boolean")関数
str_to_bool()は、文字列を入力として受け入れます。 認識された true 値のTrueを返し、認識された false 値のFalseを返し、その他の値に対してValueErrorを発生させます。同じファイルに、関数
str_to_bool()用のテストを追加します。@pytest.mark.parametrizeを使用して、真の値を最初にテストします。@pytest.mark.parametrize("string", ["Y", "y", "1", "YES"]) def test_str_to_bool_true(string): assert str_to_bool(string) is True次に、false 値を使用した別のテストを追加します。
@pytest.mark.parametrize("string", ["N", "n", "0", "NO"]) def test_str_to_bool_false(string): assert str_to_bool(string) is FalseTrueとFalseの両方の戻り値の代表的な入力をカバーする 2 つのテスト関数が追加されました。最後に、関数が受け入れてはならない値のパラメーター化されたテストを追加します。
pytest.raises()をコンテキスト マネージャーとして使用して、str_to_bool()が予期した例外を発生させる場合にのみテストが成功するようにします。@pytest.mark.parametrize("string", ["maybe", "2"]) def test_str_to_bool_invalid(string): with pytest.raises(ValueError, match="Cannot convert"): str_to_bool(string)match引数は、正規表現を使用して例外メッセージをチェックします。 ここでは、テストはメッセージの安定した部分のみをチェックするため、正確に無効な値に依存しません。
注
わかりやすくするために、この演習では、テスト対象のコードとテストを同じファイルに保持します。 実際のPython プロジェクトでは、通常、アプリケーション コードとテストは、src/ パッケージや tests/ ディレクトリなど、さまざまなファイルとディレクトリに分割されます。
ステップ 2 - テストを実行し、レポートを探索する
テストを追加したら、pytest を実行して出力を検査します。 各入力値が個別のテストとして扱われるのを確認できるように、詳細度の増加フラグ (-v) を使用します。
Windows PowerShell の場合:
python -m pytest -v test_advanced.py
macOS、Linux、またはLinux 用 Windows サブシステムの場合:
python -m pytest -v test_advanced.py
出力は次のレポートのようになります。
============================= test session starts ==============================
platform ... -- Python 3.x.y, pytest-x.y.z, pluggy-x.y.z -- ...
rootdir: ...
...
collected 10 items
test_advanced.py::test_str_to_bool_true[Y] PASSED [ 10%]
test_advanced.py::test_str_to_bool_true[y] PASSED [ 20%]
test_advanced.py::test_str_to_bool_true[1] PASSED [ 30%]
test_advanced.py::test_str_to_bool_true[YES] PASSED [ 40%]
test_advanced.py::test_str_to_bool_false[N] PASSED [ 50%]
test_advanced.py::test_str_to_bool_false[n] PASSED [ 60%]
test_advanced.py::test_str_to_bool_false[0] PASSED [ 70%]
test_advanced.py::test_str_to_bool_false[NO] PASSED [ 80%]
test_advanced.py::test_str_to_bool_invalid[maybe] PASSED [ 90%]
test_advanced.py::test_str_to_bool_invalid[2] PASSED [100%]
============================== 10 passed in 0.01s ==============================
オペレーティング システム、Pythonバージョン、pytest バージョン、実行可能ファイル のパス、ルート ディレクトリ、プラグインとキャッシュ ライン、コレクションの進行状況の書式設定、行の折り返し、実行時間が異なる場合があります。 3 つのテスト関数のみを記述しましたが、各パラメーター化された入力値は独自のテスト ケースとして実行されるため、pytest は 10 個のテストを収集しました。
手順 3 - セットアップと破棄をリファクタリングしてフィクスチャに統合する。
クラス ベースのテストを test_advanced.py ファイルに追加します。 このテストでは、xUnit スタイルの
setup_method()メソッドとteardown_method()メソッドを使用して、各テスト メソッドの周囲にファイルを作成および削除します。from pathlib import Path class TestFile: def setup_method(self): self.path = Path("done") self.path.write_text("1", encoding="utf-8") def teardown_method(self): self.path.unlink(missing_ok=True) def test_done_file(self): contents = self.path.read_text(encoding="utf-8") assert contents == "1"このテストは合格できますが、現在の作業ディレクトリに固定ファイル名を書き込み、個別のクリーンアップ コードが必要です。 テストの実行が中断された場合、またはテストが同時に実行された場合 (
pytest-xdistや重複するローカル実行など)、この方法では古いファイルを残したり、 done という名前の既存のファイルを上書きしたり、テストが相互に影響を与えたりする可能性があります。組み込みの
tmp_pathフィクスチャを使用して一時ファイルを作成するモジュール レベルのフィクスチャを追加します。 次の手順では、セットアップメソッドとティアダウンメソッドの代わりにこのフィクスチャを使用するためにクラスを更新します。 読みやすくするために、パラメーター化されたstr_to_boolテストの後、およびTestFileクラスの前に、このモジュール レベルのフィクスチャを配置します。 Pytest は、モジュール内のどこに表示されているかに関係なく、モジュール レベルのフィクスチャを検出するため、位置は pytest ではなく人間のリーダー向けです。@pytest.fixture def tmp_file(tmp_path): def write(): path = tmp_path / "done" path.write_text("1", encoding="utf-8") return path return writetmp_file()フィクスチャは pytest のtmp_pathフィクスチャを使用します。このフィクスチャは、各パラメーター化されたケースを含む、テスト関数の呼び出しごとに一意のpathlib.Path一時ディレクトリを提供します。 Pytest は一時ディレクトリを管理するため、テストはハードコーディングされたファイル パスに依存しません。TestFileメソッドやsetup_method()メソッドなど、以前のteardown_method()クラスを削除し、セットアップメソッドとティアダウンメソッドの代わりにフィクスチャを使用するこの最終バージョンに置き換えます。class TestFile: def test_done_file(self, tmp_file): path = tmp_file() contents = path.read_text(encoding="utf-8") assert contents == "1"テストで
from pathlib import Pathオブジェクトが直接作成されなくなったため、前の手順で追加したPathインポートを削除することもできます。
作業を確認
これで、test_advanced.py という名前のPython ファイルが作成されます。
- 文字列を受け取り、認識された true 値と false 値のブール値を返す
str_to_bool()関数。 -
str_to_bool()関数に対する3つのパラメータ化されたテストがあります。1つはTrueの値をテストし、1つはFalseの値をテストし、もう1つは無効な値がValueErrorを発生させるかどうかを確認します。 - 組み込みの
tmp_pathフィクスチャを使用して、いくつかのコンテンツを含む一時 完了 ファイルを作成するカスタム pytest フィクスチャ。 - カスタム
TestFileフィクスチャを使用してファイルを作成および読み取る 1 つのメソッドを持つ 1 つのtmp_fileクラス。 - インポート
from pathlib import Pathせず、最終バージョンではsetup_method()/teardown_method()メソッドがありません。
最終的なファイルは次のようになります。
import pytest
def str_to_bool(string):
normalized = string.lower()
if normalized in ["yes", "y", "1"]:
return True
if normalized in ["no", "n", "0"]:
return False
raise ValueError(f"Cannot convert {string!r} to a boolean")
@pytest.mark.parametrize("string", ["Y", "y", "1", "YES"])
def test_str_to_bool_true(string):
assert str_to_bool(string) is True
@pytest.mark.parametrize("string", ["N", "n", "0", "NO"])
def test_str_to_bool_false(string):
assert str_to_bool(string) is False
@pytest.mark.parametrize("string", ["maybe", "2"])
def test_str_to_bool_invalid(string):
with pytest.raises(ValueError, match="Cannot convert"):
str_to_bool(string)
@pytest.fixture
def tmp_file(tmp_path):
def write():
path = tmp_path / "done"
path.write_text("1", encoding="utf-8")
return path
return write
class TestFile:
def test_done_file(self, tmp_file):
path = tmp_file()
contents = path.read_text(encoding="utf-8")
assert contents == "1"
テスト ファイルをもう一度実行します。
Windows PowerShell の場合:
python -m pytest -v test_advanced.py
macOS、Linux、またはLinux 用 Windows サブシステムの場合:
python -m pytest -v test_advanced.py
最後のレポートには、11 個の収集されたテスト (パラメーター化されたテストから 10 件、クラスベースのテストが 1 つ) が表示されます。
============================= test session starts ==============================
platform ... -- Python 3.x.y, pytest-x.y.z, pluggy-x.y.z -- ...
rootdir: ...
...
collected 11 items
test_advanced.py::test_str_to_bool_true[Y] PASSED [ 9%]
test_advanced.py::test_str_to_bool_true[y] PASSED [ 18%]
test_advanced.py::test_str_to_bool_true[1] PASSED [ 27%]
test_advanced.py::test_str_to_bool_true[YES] PASSED [ 36%]
test_advanced.py::test_str_to_bool_false[N] PASSED [ 45%]
test_advanced.py::test_str_to_bool_false[n] PASSED [ 54%]
test_advanced.py::test_str_to_bool_false[0] PASSED [ 63%]
test_advanced.py::test_str_to_bool_false[NO] PASSED [ 72%]
test_advanced.py::test_str_to_bool_invalid[maybe] PASSED [ 81%]
test_advanced.py::test_str_to_bool_invalid[2] PASSED [ 90%]
test_advanced.py::TestFile::test_done_file PASSED [100%]
============================== 11 passed in 0.01s ==============================
すべてのテストはエラーなしで合格する必要があります。