はじめに
この記事では、Pythonのテストフレームワーク「Pytest」の基本的な使い方について、初心者向けに解説します。
テストを書くことでコードの品質を向上させ、バグを早期に発見できるため、品質を向上させることができます。
pytestの公式サイトへのリンク
目次
1.Pytestとは
2.Pytestのインストール
3.簡単なテストを書いてみる
4.例外のテスト
5.オプション
6.パラメータ化
7.フィックスチャー
8.前処理と後処理
9.mock
Pytestとは
Pytest とは、Python用のオープンソースのテストフレームワークです。
Pythonのコードを自動化してテストするためのツールで、特にシンプルさと柔軟性で知られています。
~特徴~
シンプルな書き方
関数名をtest_で始めるだけで、自動的にテストとして認識してくれるためシンプル。
自動検出
test_で始まるファイル名や関数名を持つものをすべて探して実行してくれる。
強力なプラグインシステム
多くのプラグインが存在し、例えばカバレッジ測定(どの部分がテストされているかの確認)や、並列テスト(複数のテストケースを同時に実行)など、さまざな追加機能を利用可能
Pytestのインストール
まずは、pytestをインストールしましょう。
pytestのインストールはターミナルで以下のコマンドを実行するだけです。
pip install pytest
インストールができたら以下のコマンドでpytestのバージョンを確認します。
pytest --version
以下のようにpytestのバージョンが表示されたら、インストールは完了です。
pytest 7.4.4
簡単なテストを書いてみる
Pytestでテストを書くにはルールが3点あります。
1.関数名はtest_で始める
Pytestは自動的にtest_で始まるか_testで終わる関数をテストとして認識するため、必ず関数名をtest_で始めます。
2.結果の確認にはassert文を使う
Pytestでは、特別なメソッドを使う必要はなく、Python標準のassertを使ってテスト結果を検証します。
条件がTrueでない場合、自動的にテストが失敗します。
3.ファイル名はtest_で始めるか、_testで終わる
Pytestは、test_で始まるか_testで終わるファイルを自動的に検出し、テストとして実行します。
では、上記のルールを基に簡単なテストを書いてみます。
def test_addition():
assert 1 + 1 == 2
次にテストの実行をしていきます。
テストの実行をするにはターミナルにて以下を実行するだけです。
pytest
実行すると以下のように「test_1.py」が[100%]になっており、1 passed(成功)していることが分かります。
============================================================================== test session starts ==============================================================================
platform win32 -- Python 3.12.4, pytest-7.4.4, pluggy-1.0.0
rootdir: C:\Users\renki\Desktop\study\Qiita
plugins: anyio-4.2.0, mock-3.14.0
collected 1 item
test_1.py . [100%]
=============================================================================== 1 passed in 0.02s ===============================================================================
上記のコードを少し、複雑にしてみます。
下記のようにtestの関数が2つあり、関数の中に結果確認がいくつかあるコードを作ってみました。
def add(a, b):
return a + b
def test_add():
assert add(1, 2) == 3
assert add(-1, 1) == 0
assert add(0, 0) == 0
def test_add_negative_numbers():
assert add(-1, -1) == -2
こちらを実行してみます。
============================================================================== test session starts ==============================================================================
platform win32 -- Python 3.12.4, pytest-7.4.4, pluggy-1.0.0
rootdir: C:\Users\renki\Desktop\study\Qiita
plugins: anyio-4.2.0, mock-3.14.0
collected 2 items
pytest\test_2.py .. [100%]
=============================================================================== 2 passed in 0.01s ===============================================================================
こちらも2passedとなっており、test_となっている2つの関数が成功していることが確認できました。
例外のテスト
ここではテストを行う際に、例外が正しく発生するか確認する方法を記載します。
例外が発生することを確認するためには pytest.raises() という関数を利用します。
実際に変数aの値を0で割る計算をして、ZeroDivisionErrorというゼロで割ろうとした際に発生する例外処理になるか確認するテストを書いていきます。
import pytest
def divide(a, b):
return a / b
def test_divide_by_zero():
with pytest.raises(ZeroDivisionError):
divide(1, 0)
===================================================================== test session starts ======================================================================
platform win32 -- Python 3.12.4, pytest-7.4.4, pluggy-1.0.0
rootdir: C:\Users\Renki\Desktop\study\Qiita
plugins: anyio-4.2.0, mock-3.14.0
collected 1 item
pytest\test_3.py . [100%]
====================================================================== 1 passed in 0.05s =======================================================================
~例外メッセージの検証~
Pytestでは、例外の発生だけでなく、発生した例外に含まれるメッセージを検証することもできます。
上記のテストにて、ZeroDivisionError が発生した際にPythonが出力する標準の例外メッセージである、「division by zero」という例外メッセージとなっているか確認します。
import pytest
def divide(a, b):
return a / b
def test_divide_by_zero_with_message():
with pytest.raises(ZeroDivisionError, match="division by zero"):
divide(1, 0)
===================================================================== test session starts ======================================================================
platform win32 -- Python 3.12.4, pytest-7.4.4, pluggy-1.0.0
rootdir: C:\Users\Renki\Desktop\study\Qiita
plugins: anyio-4.2.0, mock-3.14.0
collected 1 item
pytest\test_4.py . [100%]
====================================================================== 1 passed in 0.02s =======================================================================
オプション
Pytestには多くの便利なオプションがあります。これらのオプションを使うことで、テストの実行方法をカスタマイズしたり、特定のテストだけを実行したり、詳細なレポートを表示することができます。
①詳細出力モード(-v / --verbose)
テスト結果を詳細に表示する。
各テスト関数ごとの結果(成功・失敗・スキップなど)が一覧表示される。
pytest -v
②特定のテストを実行(ファイル指定)
特定のテストファイルだけを実行。
pytest テストファイル名
③特定のテスト関数だけを実行
特定のテスト関数のみを指定して実行。
pytest テストファイル名::関数名
④失敗したテストのみ再実行(--lf / --last-failed)
前回のテスト実行で失敗したテストだけを再度実行
pytest --lf
⑤テストが失敗したら終了(--maxfail)
テストが失敗したら即座にテスト実行を終了す。
--maxfail=nでn回失敗した時点でテストを中断します。
pytest --maxfail=値
⑥テストのカバレッジ計測(--cov)
テストがコードのどの部分をカバーしているかを確認。
pytest-covプラグインをインストールする必要がある。
pytest --cov=プロジェクト名
⑦スキップされたテストを表示(-rs)
スキップされたテストの理由を詳細に表示
pytest -rs
⑧マーカー付きのテストを実行(-m)
特定のマーカーが付いたテストだけを実行。
例えば、@pytest.mark.slowというマーカーが付いたテストのみを実行することができる。
pytest -m slow
⑨並列テスト実行(-n)
テストを並列で実行し、処理を高速化する。
pytest-xdistプラグインをインストールする必要がある。
pytest -n 値 # 値分のCPUコアを使って並列実行
⑩テスト実行中の出力をキャプチャしない(-s)
テスト実行中の標準出力をそのまま表示。
pytest -s
⑪エラーメッセージの詳細表示(--tb=short / --tb=long)
テストが失敗した際に、エラーメッセージを短く表示(short)もしくは詳細に表示(long)する。
短く表示したい場合
pytest --tb=short
詳細に表示したい場合
pytest --tb=long
⑫特定のディレクトリ内のテストを実行
特定のディレクトリ内のテストをすべて実行。
サブディレクトリ内も含めてテストを実行される。
pytest ディレクトリ名/
⑬ テスト実行時間を表示(--durations)
実行時間の長いテストを表示。
引数に指定した数だけ、実行時間の長いテストをリスト表示します。
pytest --durations=値 # 実行時間が長いテストトップ値の分を表示
⑭フィクスチャの使用状況を表示(--fixtures)
利用可能なフィクスチャの一覧を表示。
pytest --fixtures
⑮特定のキーワードを含むテストのみ実行(-k)
テスト関数名やクラス名に特定のキーワードを含むテストのみを実行
pytest -k クラス名
パラメータ化
パラメータ化 とは、1つのテスト関数に対して複数の異なる入力データを渡し、その結果を確認する仕組みです。
パラメータ化を利用することで、入力データが多くても、効率的にテストを管理 することができます。
pytestの @pytest.mark.parametrize デコレータを使って、異なるパターンを効率的にテストすることができます。
実際にパラメータ化を使って、変数aと変数bを足した結果が変数expectedになるか確認するテストを作成します。
import pytest
# テスト関数に渡すデータを指定する
@pytest.mark.parametrize("a, b, expected", [
(1, 2, 3), # a=1, b=2のとき期待される結果は3
(2, 3, 5), # a=2, b=3のとき期待される結果は5
(3, 5, 8), # a=3, b=5のとき期待される結果は8
])
def test_addition(a, b, expected):
assert a + b == expected
Pytestで実行結果
===================================================================== test session starts ======================================================================
platform win32 -- Python 3.12.4, pytest-7.4.4, pluggy-1.0.0
rootdir: C:\Users\Renki\Desktop\study\Qiita
plugins: anyio-4.2.0, mock-3.14.0
collected 3 items
pytest\test_5.py ... [100%]
====================================================================== 3 passed in 0.02s =======================================================================
パラメータ化の解説
・@pytest.mark.parametrizeの第一引数には、テスト関数の引数名を文字列で指定します。
・第二引数には、テストしたい入力データの組み合わせをリストの形式で渡します。
各タプルが1つのテストケースとなり、テストが自動的に繰り返されます。
フィックスチャー
フィクスチャー は、テストケースにおいて前提条件を準備したり、テスト後に後片付けをしたりするために使います。
再利用可能で、特定のテストケースのためにコードを準備する手間を省けます。
~フィックスチャーの基本的な使い方~
フィクスチャーは @pytest.fixture
デコレータを使って定義します。
import pytest
# サンプルデータを準備するフィクスチャー
@pytest.fixture
def sample_data():
return {"key": "value"}
# フィクスチャーを引数として渡す
def test_sample(sample_data):
# sample_data はフィクスチャーから提供された辞書データ
assert sample_data["key"] == "value"
===================================================================== test session starts ======================================================================
platform win32 -- Python 3.12.4, pytest-7.4.4, pluggy-1.0.0
rootdir: C:\Users\Renki\Desktop\study\Qiita
plugins: anyio-4.2.0, mock-3.14.0
collected 1 item
pytest\test_6.py . [100%]
====================================================================== 1 passed in 0.02s =======================================================================
~スコープ~
フィックスチャーでは、フィクスチャーがどのタイミングで実行されるか、どの程度の範囲で共有されるかを制御できます。これを スコープ といいます。
スコープは以下のように記載します。
@pytest.fixture
(scope="スコープの種類")
スコープにはいくつか種類があるので紹介します。
①function(デフォルト)
各テスト関数が実行されるたびにフィクスチャーが実行され、新しいインスタンスが提供される。
テストの独立性が高く、テスト間での状態の共有がない。
# 各テスト関数で新しい辞書を返す
# @pytest.fixture(scope="スコープの種類")にてscopeを設定する必要はない
@pytest.fixture
def sample_data():
return {"key": "value"}
②class
各テストクラスの最初のテストが実行される前にフィクスチャーが実行され、そのクラス内の全てのテストで同じインスタンスが使われる。
# クラス内のすべてのテストで同じ辞書を共有する
@pytest.fixture(scope="class")
def class_data():
return {"class_key": "class_value"}
③module
各モジュール(ファイル)内で、最初のテストが実行される前にフィクスチャーが実行され、そのモジュール内の全てのテストで同じインスタンスが使われる。
# モジュール内のすべてのテストで同じデータを共有する
@pytest.fixture(scope="module")
def module_data():
return {"module_key": "module_value"}
④session
テストセッション全体で一度だけ実行され、そのセッション内の全てのテストで同じインスタンスが使われる。
最も広い範囲で共有されるため、全てのテストで同じ準備作業や設定を行いたい場合に使用する。
# テストセッション中の全てのテストで同じ辞書を使用
@pytest.fixture(scope="session")
def session_data():
return {"session_key": "session_value"}
~Autouseフィクスチャー~
特定のテスト関数で明示的に引数として渡さなくても、自動的に適用されるフィクスチャーがあります。これを Autouseフィクスチャー といいます。
autouse=True を指定することで対象のスコープ内のすべてのテストで自動的に実行することができます。
import pytest
@pytest.fixture(autouse=True)
def enable_logging():
print("ログを有効にしました")
def test_example_1():
assert 1 == 1
def test_example_2():
assert 2 == 2
pytest -sで実行
===================================================================== test session starts ======================================================================
platform win32 -- Python 3.12.4, pytest-7.4.4, pluggy-1.0.0
rootdir: C:\Users\t2023098\Desktop\study\Qiita
plugins: anyio-4.2.0, mock-3.14.0
collected 2 items
pytest\test_7.py ログを有効にしました
.ログを有効にしました
.
====================================================================== 2 passed in 0.02s =======================================================================
上記のようにAutouseフィクスチャーを使用することで、フィックスチャーの部分が関数毎に呼び出されていることが分かります。
前処理と後処理
Pytestでテストの前処理(セットアップ)と後処理(クリーンアップ)を行いたい場合、以下の方法がよく使われます。
Pytestでは、setup_function や setup_module のようなシンプルな方法に加えて、 yield を使ったより柔軟なフィクスチャーも提供されています。
~setup_function / teardown_function (関数単位の前後処理)~
setup_function / teardown_function を用いることで、各テスト関数が実行されるたびに前処理、後処理が呼び出されます。
記載方法としては以下になります。
前処理の場合
def setup_function():
処理
後処理の場合
def teardown_function():
処理
以下の例では、各テスト関数の実行前に前処理が行われ、実行後に後処理が行われます。
def setup_function():
print("前処理(関数単位)")
def teardown_function():
print("後処理(関数単位)")
def test_func1():
print("test_func1実行")
assert True
def test_func2():
print("test_func2実行")
assert True
pytest -sで実行
===================================================================== test session starts ======================================================================
platform win32 -- Python 3.12.4, pytest-7.4.4, pluggy-1.0.0
rootdir: C:\Users\Renki\Desktop\study\Qiita
plugins: anyio-4.2.0, mock-3.14.0
collected 2 items
pytest\test_8.py
前処理(関数単位)
test_func1実行
.後処理(関数単位)
前処理(関数単位)
test_func2実行
.後処理(関数単位)
====================================================================== 2 passed in 0.02s =======================================================================
~setup_module / teardown_module (モジュール単位の前後処理)~
モジュール単位の前後処理は、モジュール内のすべてのテスト関数が実行される前に1回だけ呼び出され、後処理も1回だけ実行されます。
記載方法としては以下になります。
前処理の場合
def setup_module():
処理
後処理の場合
def teardown_module():
処理
実際にコードにして確認してみます。
def setup_module():
print("前処理(モジュール全体)")
def teardown_module():
print("後処理(モジュール全体)")
def test_func1():
print("test_func1実行")
assert True
def test_func2():
print("test_func2実行")
assert True
pytest -sで実行
===================================================================== test session starts ======================================================================
platform win32 -- Python 3.12.4, pytest-7.4.4, pluggy-1.0.0
rootdir: C:\Users\Renki\Desktop\study\Qiita
plugins: anyio-4.2.0, mock-3.14.0
collected 2 items
pytest\test_9.py
前処理(モジュール全体)
test_func1実行
.test_func2実行
.後処理(モジュール全体)
====================================================================== 2 passed in 0.03s =======================================================================
~setup_class / teardown_class (クラス単位の前後処理)~
クラス単位で前後処理を行う場合は、クラス内のすべてのテストメソッドが実行される前に前処理が、すべてのテストメソッドが終了した後に後処理が実行されます。
記載方法としては以下になります。
前処理の場合
@classmethod
def setup_class(cls):
処理
後処理の場合
@classmethod
def teardown_class(cls):
処理
実際にコードにして確認してみます。
class TestClass:
@classmethod
def setup_class(cls):
print("前処理(クラス単位)")
@classmethod
def teardown_class(cls):
print("後処理(クラス単位)")
def test_method1(self):
print("test_method1実行")
assert True
def test_method2(self):
print("test_method2実行")
assert True
===================================================================== test session starts ======================================================================
platform win32 -- Python 3.12.4, pytest-7.4.4, pluggy-1.0.0
rootdir: C:\Users\Renki\Desktop\study\Qiita
plugins: anyio-4.2.0, mock-3.14.0
collected 2 items
pytest\test_10.py
前処理(クラス単位)
test_method1実行
.test_method2実行
.後処理(クラス単位)
====================================================================== 2 passed in 0.02s =======================================================================
~ yieldを使ったフィクスチャー~
yieldを使ったフィクスチャーは、前処理と後処理を一つの関数内で定義することができます。
前処理はyieldの前に、後処理はyieldの後に記述します。
記載方法としては以下になります。
def 関数名():
前処理
yield
後処理
実際にコードにして確認してみます。
import pytest
@pytest.fixture
def resource_setup():
print("前処理")
yield
print("後処理")
def test_example(resource_setup):
print("テスト実行")
assert True
pytest -sで実行
===================================================================== test session starts ======================================================================
platform win32 -- Python 3.12.4, pytest-7.4.4, pluggy-1.0.0
rootdir: C:\Renki\t2023098\Desktop\study\Qiita
plugins: anyio-4.2.0, mock-3.14.0
collected 1 item
pytest\test_11.py
前処理
テスト実行
.後処理
====================================================================== 1 passed in 0.03s =======================================================================
mock
mock(モック) とはプログラムのテストにおいて、テスト対象のコードで利用する外部のリソースや機能(例:API、データベース、ファイルシステムなど)をコピーするオブジェクトです。
モックは本物のオブジェクトの代わりに使われ、テストの際に期待する動作を再現させたり、異常系の動作をシミュレーションするために利用されます。
~pytest-mockのインストール~
pytestにてmockを使う際には pytest-mock というモックオブジェクトを簡単に作成し、テストに利用できる機能を使用します。
pytest-mockを利用するには、ターミナルにて以下のコマンドを実行する必要があります。
pip install pytest-mock
~pytest-mockの実例~
ユーザID、パスワード、ユーザ名のモックを用意し、
ユーザIDとパスワードが正しい場合:「ユーザ名さんようこそ!」を返す
ユーザIDとパスワードが異なる場合:「ユーザ情報が違います。」と返す
関数を作成してテストしてみます。
・フォルダ構造
pytest_mock/
├─ user_data.py
├─ user_information.py
└─ test_user_information.py
・user_data.py
ユーザ情報(ユーザID、パスワード、ユーザ名)を辞書として返す関数
テスト用に固定のデータ(user001というユーザIDとtanaka001というパスワード、田中太郎というユーザ名)を返しています。
def get_user_data():
return {
"user_id": "user001",
"password": "tanaka001",
"user_name": "田中太郎"
}
・user_information.py
ユーザIDとパスワードを引数として受け取り、get_user_data()関数からユーザ情報を取得します。
引数として受け取ったuser_idとpasswordがget_user_dataから取得した値と比較して、
一致した場合:ユーザ名に「さん、ようこそ!」を付けて返します。
不一致の場合:「ユーザ情報が違います。」というエラーメッセージを返します。
from user_data import get_user_data
def user_infomation(user_id, password):
user_data = get_user_data()
if user_id == user_data["user_id"] and password == user_data["password"]:
return f"{user_data['user_name']}さん、ようこそ!"
else:
return "ユーザ情報が違います。"
・test_user_information.py
mock_user_data()関数
mocker.patch('user_data.get_user_data', return_value={...}) を使って、get_user_data関数をモックしています。これにより、本来のget_user_data関数が呼び出されず、指定された戻り値(辞書型データ)が返されます。
test_user_infomation_success()関数
正しいユーザIDとパスワードでログインが成功することを確認するテスト
test_user_infomation_fail()関数
間違ったユーザIDとパスワードでログインが失敗することを確認するテスト
from user_information import user_infomation
def mock_user_data(mocker):
mocker.patch('user_data.get_user_data', return_value={
"user_id": "user001",
"password": "tanaka001",
"user_name": "田中太郎"
})
def test_user_infomation_success(mocker):
mock_user_data(mocker)
mock_user_id = "user001"
mock_password = "tanaka001"
result = user_infomation(mock_user_id, mock_password)
assert result == "田中太郎さん、ようこそ!"
def test_user_infomation_fail(mocker):
mock_user_data(mocker)
mock_user_id = "fail"
mock_password = "fail"
result = user_infomation(mock_user_id, mock_password)
assert result == "ユーザ情報が違います。"
上記のコードをpytestでテストしてみます。
======================================================= test session starts =======================================================
platform win32 -- Python 3.12.4, pytest-7.4.4, pluggy-1.0.0
rootdir: C:\Users\Renki\Desktop\study\pytest_mock
plugins: anyio-4.2.0, mock-3.14.0
collected 2 items
test_user_information.py .. [100%]
======================================================== 2 passed in 0.02s ========================================================
正しく動いていることが分かりました。
上記のようにモックにすることによって、 外部データや実際の環境に依存せず、関数の挙動をテスト可能にでき、異常系のテストも簡単に行う ことができます。
また、本番環境になった際なども実際のデータに影響を及ぼすことがない ため安全にテストを行うことができます。
まとめ
pytestは、シンプルかつ強力なテストフレームワークで、Pythonのコードをテストするのに最適です。
この記事では基本的な使い方を紹介しましたが、より高度な機能やプラグインを使えば、さらに効率的なテストが可能になります。
私もPytestについて学び始めたばかりなので、これからの自己研鑽や業務を通して学んだことがあった際はまたこのように共有していきたいです。
長い記事となりましたが、お読みいただきありがとうございました。
参考文献
・【0から始めるPytest超基礎講座】Pythonのプログラムを効率的にテスト
・現役シリコンバレーエンジニアが教えるPython 3 入門 + 応用 +アメリカのシリコンバレー流コードスタイル
・pytest: helps you write better programs