こんにちは。もんた。です。
新しい試みとして、私がゼロベースから「完全に理解した」領域までのプロセスをそのまま記事にしたら「そのトピックを理解できる」かつ「記事いっぱい書いててなんかすごいやつ」という一石二鳥かませるんじゃね?これイノベーションじゃね?と思ったの始めてみます。
これはなに
この記事は私がpytestの理解がゼロの状態から「完全に理解した」領域に至るまでのプロセスをまとめたものです。
この記事を読んで私と一緒にpytestを理解していきましょう。
はじめに:pytestとは?
はじめに:ソフトウェア品質の要 🛡️ - テストとPytest
ソフトウェア開発において、テストはアプリケーションの品質を担保し、信頼性を高めるために不可欠なプロセスです。優れたテストは、以下の目的を果たします。
- バグの早期発見: 開発サイクルの早い段階で問題を発見し、修正コストを低減します 🐛。
- リファクタリングの促進: コードの変更が既存機能に影響を与えていないことを確認し、安心してコード改善を進められます 😌。
- 仕様のドキュメント化: テストコード自体が、コードがどのように動作すべきかの生きたドキュメントとして機能します 📖。
Pythonには標準ライブラリとして
unittest
がありますが、よりモダンで高機能、かつ記述が容易なテストフレームワークとして広く採用されているのが Pytest です ✨。Pytestの主な特徴 🚀
Pytestが多くの開発者に支持される理由は、その強力かつ洗練された機能群にあります。
1. シンプルなテスト記述 ✍️
複雑なクラス継承や定型的なコード(ボイラープレート)を必要とせず、標準の
assert
文 を使って直感的にテストを記述できます。テスト関数は
test_
プレフィックスを付けるだけで認識され、特別なデコレータ等は必須ではありません。Python
# calculation.py def add(a, b): return a + b # test_calculation.py from calculation import add def test_add(): assert add(1, 2) == 3 assert add(0, 0) == 0 assert add(-1, 1) == 0
2. 強力なフィクスチャ機能 ⚙️
- テストの**前処理(セットアップ)と後処理(ティアダウン)**をモジュール化し、再利用可能にするための強力な仕組みです。
- データベース接続、テストデータの準備、APIクライアントのモック化などをエレガントに扱えます。
- 依存性注入(Dependency Injection)の考え方に基づいており、テスト関数が必要なフィクスチャを引数として受け取るだけで利用できます。
- (フィクスチャの詳細は、今後の記事で詳しく解説します)
3. パラメータ化テスト 🔄
一つのテスト関数に対して、複数の異なる入力値と期待値の組み合わせを簡単に適用できます。
これにより、同じテストロジックを異なるデータで効率的に実行でき、網羅性を高められます。
Python
import pytest from calculation import add @pytest.mark.parametrize("input1, input2, expected", [ (1, 2, 3), (0, 5, 5), (-1, -1, -2), (10, -5, 5), ]) def test_add_parametrized(input1, input2, expected): assert add(input1, input2) == expected
4. 詳細なアサーション情報 🔍
- テストが失敗した場合、Pytestは
assert
文で比較された値の詳細を自動的に表示します。- これにより、なぜテストが失敗したのか、期待値と実際の結果がどのように異なっていたのかを迅速に把握でき、デバッグ作業を大幅に効率化します。
5. 柔軟なテスト選択・実行(マーカー) 🏷️
- テスト関数やクラスに
@pytest.mark
デコレータを使って**マーカー(ラベル)**を付与できます。- 特定のマーカーが付いたテストのみを実行したり、逆に特定のマーカーが付いたテストを除外したりするなど、テストスイートの実行を柔軟に制御できます。 (
@pytest.mark.slow
,@pytest.mark.integration
など)6. 豊富なプラグインエコシステム 🧩
- Pytestは非常に拡張性が高く、数多くのサードパーティ製プラグインが存在します。
- DjangoやFlaskなどのWebフレームワーク連携、テストカバレッジ計測 (
pytest-cov
)、非同期処理のサポート (pytest-asyncio
)、テスト実行の並列化 (pytest-xdist
) など、プロジェクトのニーズに合わせて機能を簡単に追加できます。7.
unittest
/nose
との互換性 👍
- 既存の
unittest
やnose
ベースのテストスイートを持っている場合でも、多くの場合、Pytestはそれらのテストをそのまま検出・実行できます。これにより、既存のテスト資産を活かしながら段階的にPytestへ移行することが可能です。なぜPytestを選ぶのか? 🤔 (vs
unittest
)Python標準の
unittest
と比較した場合、Pytestには以下のような利点があります。
- 記述の簡潔性: ボイラープレートが少なく、より少ないコード量でテストを記述できます。
- フィクスチャの優位性:
unittest
のsetUp/tearDown
メソッドよりも、Pytestのフィクスチャは再利用性、構成可能性、可読性の面で優れています。- 標準
assert
の利用:self.assertEqual()
のような専用のアサーションメソッドを覚える必要がなく、Python標準のassert
をそのまま利用できるため、より自然でPythonicな記述が可能です。- 豊富な機能と拡張性: パラメータ化、マーカー、強力なプラグインシステムなど、
unittest
単体では提供されない多くの便利な機能を利用できます。まとめ 📝
Pytestは、そのシンプルさ、強力な機能群、そして高い拡張性により、現代的なPython開発におけるテストフレームワークのデファクトスタンダードとしての地位を確立しています。テストの記述、実行、管理を効率化し、ソフトウェアの品質向上に大きく貢献します。
次回は、実際にPytestをインストールし、基本的なテストを作成して実行する手順を見ていきましょう!
いかがでしょうか? 表現を調整し、マークダウンと絵文字をバランス良く使用してみました。これでQiita記事の導入部分として、より適切なトーンになったかと思います。
なるほど。
pytestでは以下のようなシンプルな構文でテストコードが書けるんですね。
# calculation.py
def add(a, b):
return a + b
# test_calculation.py
from calculation import add
def test_add():
assert add(1, 2) == 3
assert add(0, 0) == 0
assert add(-1, 1) == 0
テスト関数は
test_
プレフィックスを付けるだけで認識され、特別なデコレータ等は必須ではありません。
ここからあるように、test_
プレフィックスをつけるだけで勝手にテストコードとして認識してくれるそうです。かなり楽ですね。
いくつか気になるキーワードが出てきましたね。
- 強力なフィクスチャ機能
- パラメータ化テスト
- 柔軟なテスト選択(マーカー)
unittest
との互換性
これらに関しては後ほど詳しくみていくことにしましょう。
🐶「pytestは関数にtest_とついたものを自動でテストしてくれる便利なテストフレームワーク。そのほかにもいろんな機能がある。」
つまりpytestとは?
まとめると、pytestとは標準のunittestよりも高機能で記述が簡単なテストフレームワークのようです。
以下のようなシンプルな記法でPythonコードのテストを行える優れたテストフレームワークなんですね!
# calculation.py
def add(a, b):
return a + b
# test_calculation.py
from calculation import add
def test_add():
assert add(1, 2) == 3
assert add(0, 0) == 0
assert add(-1, 1) == 0
そのほかの機能として、フィクスチャ、パラメータ化テスト、柔軟なテストの選択(マーカー)などがあるそうです。
では、実際のコードを見ていきながら、これらの機能がどういうものなのかを理解していきましょう。
簡単なテストコードを実行するまでの流れ
ではまず、簡単なサンプルコードを用いてpytestを理解していきましょう!
ではお金を使った簡単なクラスを題材に基本的なサンプルコードを作成してもらいましょう。
作ってもらったのはお金に関するサンプルコードです。
それでは、Pydantic の BaseModel を使用した「お金」クラス (Money) と、それに対する Pytest のサンプルコードを改めて提示します。
1. Pydantic
BaseModel
を使ったMoney
クラス (money_pydantic.py
)Python
# money_pydantic.py from pydantic import BaseModel, validator, Field from typing import Type # --- カスタム例外クラス --- class InsufficientFundsError(Exception): """残高不足を表すカスタム例外""" pass class DifferentCurrencyError(ValueError): """異なる通貨での操作を試みた際の例外""" pass # --- Pydantic BaseModel を使った Money クラス --- MoneyType = Type['Money'] class Money(BaseModel): """ 金額と通貨単位を持つPydanticモデル """ amount: int = Field(..., description="金額 (整数)") currency: str = Field(..., description="通貨単位 (3文字のアルファベット推奨)", min_length=1) @validator('currency') def currency_must_be_uppercase(cls, v: str) -> str: """通貨単位を大文字に変換・検証""" # 簡単なアルファベットチェック(任意) if not v.isalpha(): raise ValueError('通貨単位はアルファベットである必要があります') return v.upper() def add(self, other: MoneyType) -> MoneyType: """同じ通貨の場合のみ加算""" if self.currency != other.currency: raise DifferentCurrencyError(f"異なる通貨({self.currency}, {other.currency})同士の加算はできません") new_amount = self.amount + other.amount return Money(amount=new_amount, currency=self.currency) def subtract(self, other: MoneyType) -> MoneyType: """同じ通貨の場合のみ減算""" if self.currency != other.currency: raise DifferentCurrencyError(f"異なる通貨({self.currency}, {other.currency})同士の減算はできません") new_amount = self.amount - other.amount # 必要に応じて残高チェック # if new_amount < 0: # raise InsufficientFundsError("残高が不足しています") return Money(amount=new_amount, currency=self.currency) # Pydantic V2以降の設定 (任意) class Config: frozen = False # フィールドの再代入を許可
シンプルなお金に関するモデルを作ってもらいました。
以下の要件を持つMoneyクラスのようです。
- 通貨単位は大文字
- お金の足し算を行う(add)
- お金の引き算を行う(subtract)
ではこれをベースに、pytestを用いたテストコードを書くとどうなるのか見ていきましょう!
2. Pytest を使ったテストコード (
test_money_pydantic.py
)Python
# test_money_pydantic.py import pytest from pydantic import ValidationError from money_pydantic import Money, DifferentCurrencyError # --- 正常系のテスト --- def test_money_creation_and_attributes(): """Moneyオブジェクトの生成と属性、バリデーターのテスト""" m = Money(amount=1000, currency="jpy") assert m.amount == 1000 assert m.currency == "JPY" # 大文字に変換されているか def test_money_equality(): """Moneyオブジェクト同士の等価比較テスト""" assert Money(amount=1000, currency="JPY") == Money(amount=1000, currency="JPY") assert Money(amount=1000, currency="JPY") != Money(amount=500, currency="JPY") assert Money(amount=1000, currency="JPY") != Money(amount=1000, currency="USD") assert Money(amount=100, currency="USD") != 100 # 異なる型との比較 def test_money_addition_same_currency(): """同じ通貨同士の加算テスト""" m1 = Money(amount=1000, currency="JPY") m2 = Money(amount=500, currency="JPY") expected = Money(amount=1500, currency="JPY") assert m1.add(m2) == expected def test_money_subtraction_same_currency(): """同じ通貨同士の減算テスト""" m1 = Money(amount=1000, currency="JPY") m2 = Money(amount=300, currency="JPY") expected = Money(amount=700, currency="JPY") assert m1.subtract(m2) == expected # --- 異常系・例外のテスト --- def test_addition_different_currency_raises_error(): """異なる通貨の加算でDifferentCurrencyErrorが発生するかテスト""" m_jpy = Money(amount=1000, currency="JPY") m_usd = Money(amount=10, currency="USD") with pytest.raises(DifferentCurrencyError, match=r"異なる通貨\(JPY, USD\)同士"): # matchでエラーメッセージも検証 m_jpy.add(m_usd) def test_subtraction_different_currency_raises_error(): """異なる通貨の減算でDifferentCurrencyErrorが発生するかテスト""" m_jpy = Money(amount=1000, currency="JPY") m_usd = Money(amount=10, currency="USD") with pytest.raises(DifferentCurrencyError): # 型のみチェック m_jpy.subtract(m_usd) # --- Pydanticバリデーションエラーのテスト --- def test_creation_with_invalid_amount_type_raises_validation_error(): """不正な型の金額でValidationErrorが発生するかテスト""" with pytest.raises(ValidationError) as excinfo: Money(amount="one thousand", currency="JPY") # エラー情報からフィールド名を確認 (より詳細な検証) assert 'amount' in str(excinfo.value) assert 'value is not a valid integer' in str(excinfo.value) def test_creation_with_empty_currency_raises_validation_error(): """空の通貨単位でValidationErrorが発生するかテスト""" with pytest.raises(ValidationError) as excinfo: Money(amount=1000, currency="") assert 'currency' in str(excinfo.value) # Field(min_length=1)によるエラーメッセージ(Pydanticバージョンで多少異なる可能性あり) assert 'ensure this value has at least 1 characters' in str(excinfo.value).lower() def test_creation_with_non_alpha_currency_raises_validation_error(): """アルファベット以外の通貨単位でValidationErrorが発生するかテスト""" with pytest.raises(ValidationError) as excinfo: Money(amount=1000, currency="JP1") # バリデーターでValueErrorを発生させる assert 'currency' in str(excinfo.value) assert '通貨単位はアルファベットである必要があります' in str(excinfo.value) def test_creation_with_missing_field_raises_validation_error(): """必須フィールドが欠落している場合にValidationErrorが発生するかテスト""" with pytest.raises(ValidationError) as excinfo: Money(amount=1000) # currency を指定しない assert 'currency' in str(excinfo.value) assert 'field required' in str(excinfo.value).lower()
実行方法:
上記のコードをそれぞれ
money_pydantic.py
とtest_money_pydantic.py
という名前で、同じディレクトリに保存します。ターミナルを開き、これらのファイルが保存されているディレクトリに移動します。
以下のコマンドを実行します。
Bash
pytest
または、詳細表示する場合は
Bash
pytest -v
これにより、
test_money_pydantic.py
内のtest_
で始まる関数が自動的に実行され、結果が表示されます。すべてのテストがパスすれば、Money
クラスが期待通りに動作していること、およびバリデーションが正しく機能していることを確認できます。このサンプルコードが、Pytest と Pydantic の基本的な使い方を理解する一助となれば幸いです。
# --- 正常系のテスト ---
def test_money_creation_and_attributes():
"""Moneyオブジェクトの生成と属性、バリデーターのテスト"""
m = Money(amount=1000, currency="jpy")
assert m.amount == 1000
assert m.currency == "JPY" # 大文字に変換されているか
def test_money_equality():
"""Moneyオブジェクト同士の等価比較テスト"""
assert Money(amount=1000, currency="JPY") == Money(amount=1000, currency="JPY")
assert Money(amount=1000, currency="JPY") != Money(amount=500, currency="JPY")
assert Money(amount=1000, currency="JPY") != Money(amount=1000, currency="USD")
assert Money(amount=100, currency="USD") != 100 # 異なる型との比較
def test_money_addition_same_currency():
"""同じ通貨同士の加算テスト"""
m1 = Money(amount=1000, currency="JPY")
m2 = Money(amount=500, currency="JPY")
expected = Money(amount=1500, currency="JPY")
assert m1.add(m2) == expected
def test_money_subtraction_same_currency():
"""同じ通貨同士の減算テスト"""
m1 = Money(amount=1000, currency="JPY")
m2 = Money(amount=300, currency="JPY")
expected = Money(amount=700, currency="JPY")
assert m1.subtract(m2) == expected
正常系のテストでは通貨のバリデーション、等価比較、同じ通貨同士の加算、減算などをテストしていることがわかります。
以下の構文でアサーションを行っているんですね。なるほどなるほど。
assert A == B
テストの実行も非常に簡単ですね。
pytest
pytest -v
で簡単にpytestを実行できるんですね。
基本的な書き方は理解できましたね。
では続いてpytestを使う上で覚えておいた方がいい機能を理解していきましょう。
🐶「pytestコマンドで簡単にテストを実行することができる」
pytestを使う上で覚えておいた方が良い機能
@pytest.fixture
デコレータについて
まずは@pytest.fixture
の基本的な使い方を理解していきましょう!
Pytestフィクスチャの基本のキ!
@pytest.fixture
の使い方 ✨Pytestのフィクスチャ (
fixture
) は、テストを実行するための**「準備」**を行うための機能です。例えば、テストで使うデータを用意したり、テスト対象のオブジェクトを作成したりするのに使います。
なぜフィクスチャを使うの? 🤔
- 準備コードの共通化: 同じような準備(セットアップ)を複数のテストで行う場合、その準備コードをフィクスチャとして一つにまとめることで、コードの重複をなくし、修正も楽になります (DRY原則)。
- テストの見通し向上: テスト関数本体は「何をテストするか」というロジックに集中でき、準備のためのコードを分離できるため、テストが読みやすくなります。
- 自動的な準備: テスト関数が必要な準備を、引数で指定するだけでPytestが自動的に実行してくれます。
フィクスチャの作り方 🛠️
- 準備用の関数を定義します。
- その関数の上に
@pytest.fixture
という目印(デコレータ)を付けます。- 関数の中で必要な準備を行い、テスト関数で使いたい値(データやオブジェクトなど)を
return
で返します。Python
import pytest @pytest.fixture # ← このデコレータを付ける def user_profile(): """テストで使うユーザープロフィール(辞書)を準備して返すフィクスチャ""" print("\n (フィクスチャ user_profile 実行中...)") # デバッグ用出力 profile_data = { "id": 1, "name": "テストユーザー", "email": "test@example.com", "is_active": True } return profile_data # ← 準備したデータを返す
フィクスチャの使い方 🚀
- テストを書きたい関数(
test_
で始まる関数)を定義します。- その関数の引数に、使いたいフィクスチャの関数名をそのまま書きます。
- Pytestがテストを実行する前に、指定されたフィクスチャ関数を自動的に呼び出し、その戻り値をテスト関数の引数に渡してくれます。
Python
# 上記の user_profile フィクスチャを使うテスト関数 def test_user_name(user_profile): # ← 引数にフィクスチャ名を書く """ユーザープロフィールの名前が正しいかテスト""" print("\n (テスト test_user_name 実行中...)") # 引数 'user_profile' には、フィクスチャが返した辞書が入っている assert user_profile["name"] == "テストユーザー" def test_user_is_active(user_profile): # ← 同じフィクスチャを別のテストでも使える """ユーザーがアクティブかテスト""" print("\n (テスト test_user_is_active 実行中...)") assert user_profile["is_active"] is True
もう少し例:オブジェクトを返すフィクスチャ
Python
import pytest # money_pydantic.py がある想定 from money_pydantic import Money @pytest.fixture def default_jpy_money(): """デフォルトの日本円Moneyオブジェクトを準備するフィクスチャ""" print("\n (フィクスチャ default_jpy_money 実行中...)") return Money(amount=1000, currency="JPY") def test_initial_money_amount(default_jpy_money): # ← 引数でMoneyオブジェクトを受け取る """Moneyオブジェクトの初期金額をテスト""" print("\n (テスト test_initial_money_amount 実行中...)") assert default_jpy_money.amount == 1000 def test_initial_money_currency(default_jpy_money): """Moneyオブジェクトの初期通貨をテスト""" print("\n (テスト test_initial_money_currency 実行中...)") assert default_jpy_money.currency == "JPY"
まとめ
@pytest.fixture
の基本的な使い方はこれだけです!
@pytest.fixture
デコレータを使って準備用の関数を作る。- テスト関数の引数にその関数名を書く。
これだけで、テストの準備コードを共通化し、テスト本体をシンプルに保つことができます。まずはこの基本を押さえて、テストコードを整理整頓してみましょう!
例えば、テストで使うデータを用意したり、テスト対象のオブジェクトを作成したりするのに使います。
ここがfixtureの本質ですね。
テストコードを実行する前のテストデータの用意、オブジェクトの作成を担うのがfixtureの役割ってことですね。
テストの前準備を行う際に使うのがfixtureということですね。
自分も軽く実装した時はモックのDBなんかをfixtureを使って実装していました。
fixtureは以下のようにデコレータをつけることで定義ができます。
import pytest
@pytest.fixture # ← このデコレータを付ける
def user_profile():
"""テストで使うユーザープロフィール(辞書)を準備して返すフィクスチャ"""
print("\n (フィクスチャ user_profile 実行中...)") # デバッグ用出力
profile_data = {
"id": 1,
"name": "テストユーザー",
"email": "test@example.com",
"is_active": True
}
return profile_data # ← 準備したデータを返す
そうすることで、以下のようにテストを実行する際に、テスト関数の引数にテクスチャで定義した名前を代入することで、テストの中で使うことができるようになります。
# 上記の user_profile フィクスチャを使うテスト関数
def test_user_name(user_profile): # ← 引数にフィクスチャ名を書く
"""ユーザープロフィールの名前が正しいかテスト"""
print("\n (テスト test_user_name 実行中...)")
# 引数 'user_profile' には、フィクスチャが返した辞書が入っている
assert user_profile["name"] == "テストユーザー"
def test_user_is_active(user_profile): # ← 同じフィクスチャを別のテストでも使える
"""ユーザーがアクティブかテスト"""
print("\n (テスト test_user_is_active 実行中...)")
assert user_profile["is_active"] is True
fixtureを使うことで、事前に定義したオブジェクトを使いまわせるようになるんですね。
🐶「fixtureはテストを行う前の処理を定義することができる。セットアップのための関数=fixture!!」
@pytest.mark.*
デコレータについて
テストに目印を!
@pytest.mark
でテストを自在にコントロール 🏷️
@pytest.mark
は、Pytest におけるマーキング機能の基本的な仕組みを提供するデコレータです。テスト関数やテストクラスに対して、様々な種類の**マーカー(目印や属性)**を付与するために使われます。これにより、テストを分類したり、特定の条件下で実行を制御したり、テストの挙動自体を変更したりすることが可能になります。
なぜマーカーを使うの? 🤔
マーカーを使う主な目的は以下の通りです。
- テストの分類とグルーピング:
- 特定の機能 (
@pytest.mark.login
,@pytest.mark.search
)、テストの種類 (@pytest.mark.unit
,@pytest.mark.integration
,@pytest.mark.ui
)、あるいは実行にかかる時間 (@pytest.mark.slow
) など、自由な基準でテストを分類できます。- テストの選択実行:
- コマンドラインオプション
m
を使うことで、特定のマーカーが付いたテストだけを実行したり (pytest -m slow
)、逆に除外したり (pytest -m "not slow"
) することができます。これにより、時間がない時に高速なテストだけを実行したり、特定の機能に関するテストだけを実行したりできます。- テストの実行制御:
- 特定の条件下でテストをスキップさせたり、失敗することを期待したりするマーカーがあります。
- テストへのパラメータ供給:
@pytest.mark.parametrize
も実はマーカーの一種で、テスト関数に複数のデータセットを渡すという特別な役割を持っています。- プラグインとの連携:
- 多くの Pytest プラグインが、特定の機能を有効にしたり、挙動を制御したりするために独自のマーカーを提供しています(例:
@pytest.mark.django_db
)。主な使い方と組み込みマーカー
@pytest.mark
の後にドット (.
) を続け、マーカー名を指定します。マーカーによっては引数を取るものもあります。1. カスタムマーカーによる分類と選択実行
自由に名前を付けてテストを分類できます。
Python
import pytest @pytest.mark.user_auth # ユーザー認証関連のテスト def test_user_login_success(): # ... ログイン成功のテスト ... assert True @pytest.mark.user_auth @pytest.mark.negative # ネガティブテストでもある def test_user_login_failure(): # ... ログイン失敗のテスト ... assert True @pytest.mark.search # 検索機能のテスト @pytest.mark.slow # 時間がかかるテスト def test_complex_search_query(): import time time.sleep(2) # 時間がかかる処理をシミュレート # ... 複雑な検索のテスト ... assert True def test_normal_function(): # マーカーなしの通常のテスト assert True
実行方法:
pytest -m user_auth
:@pytest.mark.user_auth
が付いたテストのみ実行 (2件)pytest -m "not slow"
:@pytest.mark.slow
が付いていないテストのみ実行 (3件)pytest -m "user_auth and negative"
:user_auth
とnegative
の両方が付いたテストのみ実行 (1件)pytest -m "user_auth or search"
:user_auth
またはsearch
が付いたテストを実行 (3件)2. テストのスキップ (
skip
,skipif
)
@pytest.mark.skip(reason="理由")
: テストを無条件にスキップします。@pytest.mark.skipif(condition, reason="理由")
:condition
がTrue
と評価された場合にテストをスキップします。Python
import pytest import sys @pytest.mark.skip(reason="この機能はまだ実装されていません") def test_new_feature(): pass NEEDS_ADMIN = True # 仮のフラグ @pytest.mark.skipif(NEEDS_ADMIN is False, reason="管理者権限が必要なテストです") def test_admin_only_operation(): pass @pytest.mark.skipif(sys.platform == 'win32', reason="Windowsでは動作しないテスト") def test_linux_specific_feature(): pass
3. 失敗を期待するテスト (
xfail
)
@pytest.mark.xfail(reason="理由", strict=False)
: テストが失敗することを期待します。
- テストが実際に失敗した場合:
XFAIL
(期待通り失敗) と記録されます。- テストが予期せず成功した場合:
XPASS
(予期せず成功) と記録されます。strict=True
を指定すると、XPASS
はテスト失敗として扱われます。- バグが既知だが未修正の場合や、まだ実装されていない機能のテストに一時的に使われます。
Python
import pytest @pytest.mark.xfail(reason="既知のバグ #123 が修正されるまで失敗する") def test_known_bug(): assert 1 / 0 # ZeroDivisionError が発生する @pytest.mark.xfail(strict=True, reason="この機能はまだ完全に動作しないはず") def test_unstable_feature(): # たまに成功してしまうかもしれない不安定な機能 import random assert random.choice([True, False])
4. パラメータ化 (
parametrize
)これは前の解説で詳しく説明しましたが、これも
@pytest.mark.
で始まるマーカーの一種です。Python
@pytest.mark.parametrize(...) def test_something(...): pass
カスタムマーカーの登録 ✍️
上で
user_auth
やslow
のような独自のマーカーを使いましたが、Pytestは未登録のマーカーが使われるとデフォルトで警告 (PytestUnknownMarkWarning
) を出します。この警告を抑制し、マーカーの意味を明確にするために、プロジェクトの設定ファイル (
pytest.ini
またはpyproject.toml
) でカスタムマーカーを登録することが推奨されます。例 (
pyproject.toml
):Ini, TOML
[tool.pytest.ini_options] markers = [ "slow: 時間のかかるテストにマークします", "user_auth: ユーザー認証関連のテスト", "search: 検索機能関連のテスト", "negative: ネガティブシナリオのテスト" ]
例 (
pytest.ini
):Ini, TOML
[pytest] markers = slow: 時間のかかるテストにマークします user_auth: ユーザー認証関連のテスト search: 検索機能関連のテスト negative: ネガティブシナリオのテスト
登録しておくと、
pytest --markers
コマンドで利用可能なマーカーとその説明の一覧を確認できます。まとめ
@pytest.mark
は、テストに様々な「意味」や「属性」を与えるための強力な仕組みです。
- テストの分類、選択実行 (
m
)- スキップ (
skip
,skipif
)- 失敗期待 (
xfail
)- パラメータ化 (
parametrize
)などの機能を提供し、テストスイートの管理、実行効率の向上、CI/CDとの連携などに大きく貢献します。特にカスタムマーカーを適切に使うことで、大規模なテストプロジェクトでも整理された状態を保つのに役立ちます。ぜひ活用してみてください!
以下の部分が非常にわかりやすいですね。
テストの分類とグルーピング:
- 特定の機能 (
@pytest.mark.login
,@pytest.mark.search
)、テストの種類 (@pytest.mark.unit
,@pytest.mark.integration
,@pytest.mark.ui
)、あるいは実行にかかる時間 (@pytest.mark.slow
) など、自由な基準でテストを分類できます。テストの選択実行:
- コマンドラインオプション
m
を使うことで、特定のマーカーが付いたテストだけを実行したり (pytest -m slow
)、逆に除外したり (pytest -m "not slow"
) することができます。これにより、時間がない時に高速なテストだけを実行したり、特定の機能に関するテストだけを実行したりできます。
タグみたいに、@pytest.mark.{TAG_NAME}
とすることで、テスト時に特定のタグだけ実行するみたいなことを実現するのがmark
の役割です。
覚えておくと良いのは、特定のテストケースに以下のマークをつけて
@pytest.mark.{TAG_NAME}
テスト実行時に
pytest -m {TAG_NAME}
# または
pytest -m "not {TAG_NAME}"
とすればいいということですね。
🐶「テストにタグをつけたい時に使うのが@pytest.mark!!-mオプションで特定のマークのものだけテストができる」
@pytest.mark.parametrize
デコレータについて
テストを効率化!
@pytest.mark.parametrize
で楽々データ駆動テスト 🚀
@pytest.mark.parametrize
は、一つのテスト関数を、複数の異なる入力データや期待値の組み合わせで実行したい 場合に使用するデコレータです。これにより、同じようなテストロジックをデータの数だけコピー&ペーストする必要がなくなり、コードが非常にスッキリします。これは「データ駆動テスト (Data-Driven Testing)」と呼ばれるアプローチを簡単に実現する方法の一つです。なぜ
@pytest.mark.parametrize
を使うの? 🤔
- コードの重複削減 (DRY): 同じテストロジックを何度も書く必要がなくなります。テスト対象の関数と、テストしたいデータの組み合わせを分離できます。
- 網羅性の向上: 様々な入力パターンや境界値に対するテストを、少ないコード量で簡単に記述・追加できます。
- 可読性と保守性: テストしたいデータのパターンが一箇所にまとまるため、どのようなケースをテストしているのかが分かりやすくなり、データの追加や修正も容易になります。
基本的な使い方 📖
@pytest.mark.parametrize(argnames, argvalues)
の形でテスト関数の上に記述します。
argnames
(引数名の文字列):
- テスト関数に渡したい引数の名前を、カンマ区切りの文字列 (例:
"input1, expected"
) または文字列のリスト/タプル (例:["input1", "expected"]
) で指定します。argvalues
(値のリスト):
- テストで使いたいデータの組み合わせをリストで指定します。
- リストの各要素は、
argnames
で指定した引数の数に対応する値を持つタプルまたはリストです。- Pytestは、
argvalues
の各要素を使って、テスト関数を要素の数だけ繰り返し実行します。各実行で、要素内の値がargnames
で指定された引数に渡されます。サンプルコード 📝
例1: 単純な足し算関数のテスト
Python
import pytest def add(a, b): """簡単な足し算関数""" return a + b # add関数を複数のデータの組み合わせでテスト @pytest.mark.parametrize("input1, input2, expected_sum", [ (1, 2, 3), # ケース1: 1 + 2 = 3 (0, 5, 5), # ケース2: 0 + 5 = 5 (-1, 1, 0), # ケース3: -1 + 1 = 0 (100, 200, 300), # ケース4: 100 + 200 = 300 (-5, -10, -15), # ケース5: -5 + (-10) = -15 ]) def test_addition(input1, input2, expected_sum): """add関数のテスト""" print(f"\nテスト中: add({input1}, {input2}) == {expected_sum}") # どのデータで実行されているか確認 assert add(input1, input2) == expected_sum
このコードを実行すると (
pytest -s -v
)、test_addition
関数がargvalues
で指定された5つのデータセットそれぞれに対して実行され、計5回のテストが行われます。例2: 文字列の長さをチェックするテスト
Python
import pytest @pytest.mark.parametrize("text, expected_length", [ ("", 0), # 空文字列 ("pytest", 6), # 通常の文字列 ("こんにちは", 5), # 日本語文字列 ]) def test_string_length(text, expected_length): """文字列の長さをテスト""" assert len(text) == expected_length
少し便利な使い方 (オプション) ✨
ids
: 各パラメータセットに分かりやすい名前を付けることができます。テストが失敗したときに、どのデータセットで失敗したのかがレポートで識別しやすくなります。Python
@pytest.mark.parametrize("input1, input2, expected", [ (1, 1, 2), (2, 3, 5), ], ids=["1+1=2", "2+3=5"]) # 各テストケースに名前を付ける def test_add_with_ids(input1, input2, expected): assert add(input1, input2) == expected
pytest.param
: 特定のパラメータセットにマーカー (例:xfail
やskipif
) を適用したい場合に使います。Python
@pytest.mark.parametrize("input_val, expected", [ (10, 100), (0, 0), pytest.param(-1, 1, marks=pytest.mark.xfail(reason="負数は未対応")), # このデータだけxfail扱い pytest.param("abc", None, marks=pytest.mark.skipif(sys.version_info < (3, 8), reason="文字列入力は3.8以降")) # 条件付きスキップ ]) def test_square(input_val, expected): # input_valの二乗を返す関数をテストする想定 # ... (関数の実装) pass # ここでは省略
まとめ
@pytest.mark.parametrize
は、同じようなテストを異なるデータで効率的に実行するための強力なツールです。
- テストコードの重複を劇的に減らせる。
- 様々な入力パターンを簡単にテストできる。
- テストデータが一箇所にまとまるため、管理しやすい。
ぜひ活用して、テストの効率と網羅性を高めてください!
@pytest.mark.parametrize
は複数の異なるデータや期待値を組み合わせる際に使うデコレータです。
以下のように@pytest.mark.parametrize()
を用いて事前にテスト関数の組み合わせを定義しておくことで少ない記述方法で複数のテストケースをテストすることができます。
import pytest
def add(a, b):
"""簡単な足し算関数"""
return a + b
# add関数を複数のデータの組み合わせでテスト
@pytest.mark.parametrize("input1, input2, expected_sum", [
(1, 2, 3), # ケース1: 1 + 2 = 3
(0, 5, 5), # ケース2: 0 + 5 = 5
(-1, 1, 0), # ケース3: -1 + 1 = 0
(100, 200, 300), # ケース4: 100 + 200 = 300
(-5, -10, -15), # ケース5: -5 + (-10) = -15
])
def test_addition(input1, input2, expected_sum):
"""add関数のテスト"""
print(f"\nテスト中: add({input1}, {input2}) == {expected_sum}") # どのデータで実行されているか確認
assert add(input1, input2) == expected_sum
以下が文法ですね。
@pytest.mark.parametrize(argnames, argvalues)
-
argnames
:カンマ区切りの文字列またはタプルで指定(例)
”input1, input2”
or[”input1”, “input2”]
-
argvalues
:テストしたい値のリスト(例)
[ (1, 2), (3, 6), (-10, 5) ]
その他にもids
というオプションをつけることで、それぞれのパラメータに対して名前をつけることができます。
@pytest.mark.parametrize("input1, input2, expected", [
(1, 1, 2),
(2, 3, 5),
], ids=["1+1=2", "2+3=5"]) # 各テストケースに名前を付ける
def test_add_with_ids(input1, input2, expected):
assert add(input1, input2) == expected
これはテストケースのデバッグの時にめちゃくちゃ楽ですね。
🐶「複数のパターンでテストしたい時に使えるのが@pytest.mark.parametrize」
pytest.mock
でモックを使いこなそう
依存関係を断ち切る!
pytest-mock
とmocker
フィクスチャで快適モックテスト 🎭テストを書いていると、テストしたいコードが外部のシステム(データベース、Web API、ファイルシステムなど)や、まだ完成していない他の部品、あるいは非常に複雑な別のクラスに依存していることがあります。これらの依存関係があると、テストが以下のような問題を抱えがちです。
- 遅い: 外部APIへのアクセスなどは時間がかかります。
- 不安定: 外部サービスの調子が悪かったり、ネットワークが不安定だったりするとテストが失敗します。
- 準備が大変: テストのためにデータベースに特定のデータを入れたり、外部サービスを特定の状態にするのが難しいことがあります。
- ユニットテストにならない: テストしたい対象(ユニット)だけを純粋にテストできず、依存先の挙動も含めた統合テストのようになってしまいます。
これらの問題を解決するために使われるのが モック (Mock) です。モックは、依存するオブジェクトや関数の「偽物」を作り出し、本物の代わりにそれを使うことで、依存関係を断ち切るテクニックです。
pytest-mock
は、Python 標準のunittest.mock
ライブラリを Pytest でより簡単に、便利に使うためのプラグインです。
pytest-mock
プラグインのインストールまず、プラグインをインストールする必要があります。
Bash
pip install pytest-mock
mocker
フィクスチャの使い方
pytest-mock
をインストールすると、mocker
という名前のフィクスチャが自動的に利用可能になります。テスト関数でmocker
を引数として受け取るだけで準備完了です。Python
def test_something_with_mock(mocker): # 引数で mocker を受け取る # ここで mocker を使ってモック処理を行う pass
mocker
オブジェクトは、unittest.mock
の機能をラップした便利なメソッドを提供します。
mocker
の主要なメソッドと使い方1.
mocker.patch()
- オブジェクトや関数を置き換えるこれが最もよく使われる機能です。指定したオブジェクトの属性やメソッド、あるいはモジュール内の関数などを、一時的にモックオブジェクトに置き換えます。テスト関数の終了時に、自動的に元の状態に戻してくれます。
return_value
: モックオブジェクトが呼び出されたときに返す値を指定します。side_effect
: モックが呼び出されたときの複雑な挙動(例: 呼び出されるたびに違う値を返す、特定の例外を発生させるなど)を指定します。サンプルコード: 外部API呼び出しをモックする
外部の天気APIを呼び出す関数
get_weather_data
をテストしたいとします。実際のAPIを叩かずにテストしたい場合です。Python
# weather_service.py (テスト対象の例) import requests def get_weather_data(city): try: # 本当はここで外部APIを呼び出す response = requests.get(f"https://api.example.com/weather?city={city}") response.raise_for_status() # エラーなら例外発生 return response.json() # JSONデータを返す except requests.exceptions.RequestException as e: print(f"APIエラー: {e}") return None # test_weather_service.py import pytest # weather_service.py がある想定 from weather_service import get_weather_data import requests # requests モジュールも import しておく def test_get_weather_data_success(mocker): """天気データが正常に取得できるケースをモックでテスト""" # 'weather_service.requests.get' をモックに置き換える # このテスト関数内では、requests.get は偽物になる mock_response = mocker.MagicMock() # requests.Response オブジェクトの代わり mock_response.raise_for_status.return_value = None # raise_for_status は何もしない mock_response.json.return_value = {"temperature": 25, "description": "晴れ"} # json() が返す値を設定 mock_get = mocker.patch('weather_service.requests.get', return_value=mock_response) # テスト対象の関数を実行 city = "Tokyo" weather = get_weather_data(city) # アサーション assert weather is not None assert weather["temperature"] == 25 assert weather["description"] == "晴れ" # モックが期待通りに呼び出されたか検証 mock_get.assert_called_once_with(f"https://api.example.com/weather?city={city}") mock_response.json.assert_called_once() def test_get_weather_data_api_error(mocker): """APIエラーが発生するケースをモックでテスト""" # requests.get が RequestException を発生させるように設定 mock_get = mocker.patch('weather_service.requests.get', side_effect=requests.exceptions.RequestException("接続エラー")) # テスト対象の関数を実行 weather = get_weather_data("Osaka") # アサーション assert weather is None # エラー時は None が返るはず # モックが呼び出されたか検証 mock_get.assert_called_once()
ポイント:
mocker.patch('モジュール名.関数名やクラス名')
で、対象の関数などをモックに置き換えます。文字列で指定するのが重要です。return_value
で、モックが呼び出された際の戻り値を固定します。side_effect
で、例外を発生させたり、複雑な動作をさせたりできます。mock_object.assert_called_once_with(...)
などで、モックが期待通りに使われたか(呼び出し回数、引数など)を検証できます。2.
mocker.patch.object()
- 特定オブジェクトのメソッドを置き換えるクラスのインスタンスなど、特定のオブジェクトが持つメソッドだけを置き換えたい場合に使います。
Python
class Notifier: def send_email(self, to, subject, body): # 実際にはメール送信処理 print(f"メール送信 to {to}: {subject}") return True def process_order(order_id, notifier): # ... 注文処理 ... print(f"注文 {order_id} を処理しました。") # 処理完了後にメール通知 success = notifier.send_email("customer@example.com", f"注文完了 {order_id}", "ありがとうございました。") return success def test_process_order_sends_email(mocker): notifier_instance = Notifier() # notifier_instance の send_email メソッドだけをモック化 mock_send_email = mocker.patch.object(notifier_instance, 'send_email', return_value=True) process_order("Order123", notifier_instance) # send_email が特定の引数で1回だけ呼び出されたか検証 mock_send_email.assert_called_once_with( "customer@example.com", "注文完了 Order123", "ありがとうございました。" )
3.
mocker.spy()
- 実際の処理を呼びつつ監視 (スパイ)モックと似ていますが、スパイは実際のメソッドを実行しつつ、その呼び出し状況(引数、回数など)を記録します。実際の動作はさせたいが、呼び出されたこと自体を確認したい場合に便利です。
Python
def test_process_order_spy_email(mocker): notifier_instance = Notifier() # send_email をスパイする (実際のメール送信処理(print)も実行される) spy_send_email = mocker.spy(notifier_instance, 'send_email') process_order("Order456", notifier_instance) # 呼び出されたことを確認 (引数なども検証可能) spy_send_email.assert_called_once()
まとめ
pytest-mock
プラグインとmocker
フィクスチャは、Pytest でモックテストを簡単かつ効果的に行うための強力なツールです。
mocker.patch()
で依存関係を偽のオブジェクトに置き換える。return_value
やside_effect
でモックの挙動を制御する。assert_called_with()
などでモックの利用状況を検証する。これにより、テスト対象のコードだけを分離してテスト(ユニットテスト)することが容易になり、テストの速度、安定性、信頼性を大幅に向上させることができます。依存関係を持つコードをテストする際には、ぜひ活用を検討してみてください。
テストコードを書いている時ってモックを使いたい時がありますよね。
そういう時に使えるのがこのpytest-mock
プラグインとmocker
フィクスチャです。
pytest-mock
プラグインは以下のコマンドでインストールできます。
pip install pytest-mock
pytest-mock
プラグインをインストールすることで、テストコードにて以下の書き方が利用可能になります。
def test_func_1(mocker):
# ここにmockerを使った処理を書く
引数にmockerを受け取ることができるようになります。
mockerを受け取ると何がいいのかを解説していきます。
以下のコードを参考に解説していきます。
# weather_service.py (テスト対象の例)
import requests
def get_weather_data(city):
try:
# 本当はここで外部APIを呼び出す
response = requests.get(f"https://api.example.com/weather?city={city}")
response.raise_for_status() # エラーなら例外発生
return response.json() # JSONデータを返す
except requests.exceptions.RequestException as e:
print(f"APIエラー: {e}")
return None
この関数は外部の転記情報を取得するAPIを叩く関数ですね。
では、テストコードを書く上でこの外部APIを叩かないでテストしたい場合を考えてみましょう。
mockを使う上で重要なのが、mock.patch()
です。
mock.patch()
は最も使われる機能かと思います。
mock.patch()
には一時的に指定したオブジェクトや関数などをモックに置き換えてくれるという機能があります。
mock.patch()
のサンプルコードは以下になります。これをベースにmock.patch()
を解説していきます。
# test_weather_service.py
import pytest
# weather_service.py がある想定
from weather_service import get_weather_data
import requests # requests モジュールも import しておく
def test_get_weather_data_success(mocker):
"""天気データが正常に取得できるケースをモックでテスト"""
# 'weather_service.requests.get' をモックに置き換える
# このテスト関数内では、requests.get は偽物になる
mock_response = mocker.MagicMock() # requests.Response オブジェクトの代わり
mock_response.raise_for_status.return_value = None # raise_for_status は何もしない
mock_response.json.return_value = {"temperature": 25, "description": "晴れ"} # json() が返す値を設定
mock_get = mocker.patch('weather_service.requests.get', return_value=mock_response)
# テスト対象の関数を実行
city = "Tokyo"
weather = get_weather_data(city)
# アサーション
assert weather is not None
assert weather["temperature"] == 25
assert weather["description"] == "晴れ"
# モックが期待通りに呼び出されたか検証
mock_get.assert_called_once_with(f"https://api.example.com/weather?city={city}")
mock_response.json.assert_called_once()
-
mocker.MagicMock()
:Pythonのモック機能を使う上で必要なクラス -
return_value
:モックオブジェクトが呼び出された時に返す値を設定する時に使う
以下の部分が非常に重要です。
mock_response = mocker.MagicMock() # requests.Response オブジェクトの代わり
mock_response.raise_for_status.return_value = None # raise_for_status は何もしない
mock_response.json.return_value = {"temperature": 25, "description": "晴れ"} # json() が返す値を設定
mock_get = mocker.patch('weather_service.requests.get', return_value=mock_response)
ここではモックの振る舞いを定義しています。
例外時はNoneを返すので、mock_response.raise_for_status.return_value = None
を指定。
json()
が呼ばれた時の返り値をmock_response.json.return_value = {"temperature": 25, "description": "晴れ"}
で指定しています。
そして、特に重要なのがmocker.patch()
です。
ここでは**『weather_service
のrequests.get()
が呼ばれた場合、return_value
としてmock_response
を使うようにしてね。』**ということを指定しています。
ここめっちゃ重要です。
mocker.patch()
で指定された関数が呼ばれた時に、事前に定義したモックと置き換えてねという指定をここでしているんです。
これでモックを用いたテストの準備が整いました。
その後のテストではcityを指定し、get_weather_data()
でデータをとってきています。
事前定義で{"temperature": 25, "description": "晴れ"}
と指定しているので、temperature
, description
が期待するものかどうかを確認しています。
# テスト対象の関数を実行
city = "Tokyo"
weather = get_weather_data(city)
# アサーション
assert weather is not None
assert weather["temperature"] == 25
assert weather["description"] == "晴れ"
逆に、エラーを期待する場合は以下のように例外を期待するように設定することもできます。
def test_get_weather_data_api_error(mocker):
"""APIエラーが発生するケースをモックでテスト"""
# requests.get が RequestException を発生させるように設定
mock_get = mocker.patch('weather_service.requests.get', side_effect=requests.exceptions.RequestException("接続エラー"))
# テスト対象の関数を実行
weather = get_weather_data("Osaka")
# アサーション
assert weather is None # エラー時は None が返るはず
-
side_effect
:モックが呼び出された時の挙動を指定する(例外を発生させる、違う値を返すなど)
side_effect
で例外を必ず返すように指定します。
そうすることで、「weather_service.requests.get()
は必ず例外を返す関数」となります。
関数のテストではエラーが返ってくることがわかっているので、weather is None
であることをアサーションしたらOKということです。
🐶「モックを使いこなすものがテストコードを制す!!」
with pytest.raises
とは
テストをしている時に、意図的に例外を発生させたい時ってありますよね。
そういった時に使えるのがこのpytest.raises
です。
エラー処理をテスト!
pytest.raises
で期待通りの例外発生を確認 ⚠️プログラムを書いていると、不正な入力があった場合や、予期せぬ状況になった場合に、意図的に例外 (Exception) を発生させて処理を中断させたいことがありますよね。
pytest.raises
は、そのような「特定の状況で、ちゃんと設計通りに例外が発生するかどうか」をテストするための非常に便利な機能です。なぜ
pytest.raises
を使うの? 🤔
- エラーハンドリングの検証: 関数やメソッドが、エラー条件を正しく検知し、適切な種類の例外を送出(raise)するかどうかを確認できます。
- 堅牢性の確認: 予期せぬ入力や状態に対して、プログラムがクラッシュするのではなく、制御された方法(例外送出)で応答することを確認できます。
- テストコードの簡潔化:
try...except
を使って手動で例外をチェックするよりも、はるかに簡潔かつ明確に例外テストを記述できます。基本的な使い方 📖
pytest.raises
は コンテキストマネージャ としてwith
文と一緒に使います。Python
import pytest def divide(a, b): if b == 0: raise ZeroDivisionError("ゼロで割ることはできません!") return a / b def test_divide_by_zero(): # この with ブロック内で ZeroDivisionError が発生することを期待する with pytest.raises(ZeroDivisionError): divide(10, 0) # ここで ZeroDivisionError が発生すればテスト成功! # もし divide(10, 0) が例外を発生させなかったり、 # 違う種類の例外 (例: TypeError) を発生させたりした場合は、 # このテストは失敗します。 def test_divide_normal(): # 例外が発生しないケースは普通に assert でテストする assert divide(10, 2) == 5
ポイント:
with pytest.raises(期待する例外クラス):
のように書きます。with
ブロックの中で実行したコードが、指定した例外クラス(またはそのサブクラス)の例外を発生させれば、テストは成功とみなされます。応用的な使い方 ✨
1. エラーメッセージの内容も検証したい
例外の種類だけでなく、エラーメッセージの内容も重要である場合があります。
pytest.raises
では、発生した例外オブジェクトにアクセスしてメッセージを検証できます。方法A:
as excinfo
を使う
with pytest.raises(...) as excinfo:
と書くと、発生した例外に関する情報がexcinfo
という変数(pytest.ExceptionInfo
オブジェクト)に格納されます。excinfo.value
で実際の例外インスタンスにアクセスできます。Python
# money_pydantic.py がある想定 from money_pydantic import Money, DifferentCurrencyError def test_add_different_currency_error_message_excinfo(): m_jpy = Money(amount=1000, currency="JPY") m_usd = Money(amount=10, currency="USD") with pytest.raises(DifferentCurrencyError) as excinfo: # 'as excinfo' を追加 m_jpy.add(m_usd) # excinfo.value に発生した例外インスタンスが入っている assert "異なる通貨(JPY, USD)同士の加算はできません" in str(excinfo.value) # 例外オブジェクトの属性にアクセスすることも可能 (もしあれば) # assert excinfo.value.currency1 == "JPY"
方法B:
match
引数を使う (より推奨)
pytest.raises
のmatch
引数に文字列や正規表現パターンを指定すると、発生した例外のエラーメッセージがそのパターンにマッチするかどうかを直接検証できます。こちらの方がコードがより簡潔になります。Python
def test_divide_by_zero_with_message_match(): with pytest.raises(ZeroDivisionError, match="ゼロで割ることはできません!"): divide(10, 0) def test_add_different_currency_error_message_match(): m_jpy = Money(amount=1000, currency="JPY") m_usd = Money(amount=10, currency="USD") # 正規表現を使ってメッセージの一部を検証する例 with pytest.raises(DifferentCurrencyError, match=r"異なる通貨\(JPY, USD\)"): m_jpy.add(m_usd)
注意点 🚨
with pytest.raises(...)
ブロック内で例外が発生すると、その時点でブロックの実行は中断されます。例外が発生すると期待されるコードの後に、実行されるべきコード(例えば、状態が元に戻っているかのチェックなど)を書かないように注意してください。そのようなチェックはwith
ブロックの外で行うべきです。まとめ
pytest.raises
は、関数やメソッドが期待通りに例外を発生させるかどうかをテストするための、非常に便利で読みやすい方法を提供します。
with pytest.raises(ExpectedException):
で例外の発生をチェック。match="パターン"
でエラーメッセージの内容も簡潔に検証。エラーハンドリングはプログラムの堅牢性にとって重要です。
pytest.raises
を活用して、例外処理が正しく機能していることをしっかりとテストしましょう!
基本的な使い方にもあるように、pytest.raises
はwith
と一緒に使います。
基本的な使い方 📖
pytest.raises
は コンテキストマネージャ としてwith
文と一緒に使います。
以下のコードが大変わかりやすいですね。
test_divide_by_zero()
では0で割ろうとした場合、エラーを発生させることを確認しようとしています。
import pytest
def divide(a, b):
if b == 0:
raise ZeroDivisionError("ゼロで割ることはできません!")
return a / b
def test_divide_by_zero():
# この with ブロック内で ZeroDivisionError が発生することを期待する
with pytest.raises(ZeroDivisionError):
divide(10, 0) # ここで ZeroDivisionError が発生すればテスト成功!
# もし divide(10, 0) が例外を発生させなかったり、
# 違う種類の例外 (例: TypeError) を発生させたりした場合は、
# このテストは失敗します。
def test_divide_normal():
# 例外が発生しないケースは普通に assert でテストする
assert divide(10, 2) == 5
これをtry…except…
を使って書くこともできますが、それは面倒ですよね。
そこでwith pytest.raises({EXCEPTION_NAME})
を使うことで、楽に例外時のテストができてしまうのです。
with pytest.raises({EXCEPIOTN_NAME}):
test_func() # この関数内で{EXCEPTION_NAME}の例外が発生することを期待している
エラーを期待するテストの時はpytest.raises()
を使うってことだけ覚えておけばOKです。
🐶「エラー時の挙動をテストしたい時に使えるのがpytest.raises()
!!」
conftest.py
テストの「おまとめ役」!
conftest.py
ファイルの役割 🤝
conftest.py
は、Pytestがテストを実行する際に特別に認識するファイル名です。このファイルを使うことで、テストコードをより整理し、効率的にすることができます。
conftest.py
の一番大事な役割:フィクスチャの共有 ✨これが
conftest.py
の最も一般的で強力な使い方です。
- 共通の準備コードを一箇所に: 複数のテストファイルで同じような準備(セットアップ)が必要になることはよくありますよね?例えば、テスト用のデータベース接続を用意したり、テスト対象のクラスの基本的なインスタンスを作成したり。このような共通のフィクスチャ (
@pytest.fixture
) をconftest.py
ファイルの中に定義します。import
不要で使える:conftest.py
に定義されたフィクスチャは、そのconftest.py
が置かれているディレクトリ、およびその配下の全てのテストファイルから、import
文を書かなくても自動的に利用可能になります。テスト関数でフィクスチャ名を引数に指定するだけでOKです。- メリット:
- DRY (Don't Repeat Yourself): 同じフィクスチャの定義をあちこちのテストファイルに書く必要がなくなり、コードの重複が減ります。
- 可読性向上: テストファイルはテストのロジックそのものに集中でき、準備のためのコードは
conftest.py
に分離されるため、見通しが良くなります。- 保守性向上: 共通の準備内容を変更したい場合、
conftest.py
の一箇所を修正すれば済みます。簡単な例:
Python
# tests/conftest.py (テストディレクトリのトップなどに置く) import pytest @pytest.fixture(scope='session') # セッション中1回だけ実行 def common_test_data(): """複数のテストで使う共通のテストデータ""" print("\n [conftest]共通テストデータを準備します...") return {"app_name": "My Awesome App", "version": "1.0"} @pytest.fixture def default_user(): """デフォルトのユーザー情報を返すフィクスチャ""" print("\n [conftest]デフォルトユーザーを準備します...") return {"id": 1, "name": "Default User"} # tests/test_module_a.py # common_test_data や default_user を import する必要はない! def test_app_name_in_module_a(common_test_data): assert common_test_data["app_name"] == "My Awesome App" def test_user_id_in_module_a(default_user): assert default_user["id"] == 1 # tests/feature_x/test_module_b.py (サブディレクトリでもOK) # こちらのファイルからも conftest.py のフィクスチャが使える def test_version_in_module_b(common_test_data): assert common_test_data["version"] == "1.0" def test_user_name_in_module_b(default_user): assert default_user["name"] == "Default User"
その他の役割 (補足) 🧩
conftest.py
はフィクスチャの共有以外にも、以下のような役割を担うことがあります。
- フック関数の実装: Pytestのテスト実行プロセス(テストの収集、実行、レポートなど)に割り込んで、動作をカスタマイズするための特別な関数(フック関数)を定義できます。(少し高度な使い方です)
- ローカルプラグイン: プロジェクト固有のテスト用ヘルパー関数や、小規模なプラグインのような機能を実装する場所としても使えます。
どこに置くの? 📁
conftest.py
ファイルは、通常、テストコードが置かれているディレクトリ(例えば、プロジェクトルートのtests/
ディレクトリ)や、そのサブディレクトリに配置します。conftest.py
は階層的に配置でき、内側のディレクトリにあるconftest.py
は外側のconftest.py
の設定やフィクスチャを引き継ぎつつ、独自のものを定義できます。まとめ
conftest.py
は、Pytestのテストスイートを整理し、効率化するための中心的な役割を担うファイルです。特に、複数のテストファイルで共通して利用したいフィクスチャを定義することで、コードの重複をなくし、保守性を高める上で非常に役立ちます。テストコードが増えてきたら、ぜひconftest.py
の活用を検討してみてください!
以下がconftest.py
のすべてです。これだけ覚えていたらOKですぶっちゃけ。
conftest.py
の一番大事な役割:フィクスチャの共有 ✨これが
conftest.py
の最も一般的で強力な使い方です。
- 共通の準備コードを一箇所に: 複数のテストファイルで同じような準備(セットアップ)が必要になることはよくありますよね?例えば、テスト用のデータベース接続を用意したり、テスト対象のクラスの基本的なインスタンスを作成したり。このような共通のフィクスチャ (
@pytest.fixture
) をconftest.py
ファイルの中に定義します。import
不要で使える:conftest.py
に定義されたフィクスチャは、そのconftest.py
が置かれているディレクトリ、およびその配下の全てのテストファイルから、import
文を書かなくても自動的に利用可能になります。テスト関数でフィクスチャ名を引数に指定するだけでOKです。
conftest.py
はすべてのテストコードで共通して使うfixture
や設定を定義する場所です。
また、conftest.py
はpytestをする上で意味のある名前となっているので、import不要で使えるファイルです。
🐶「パスの設定とか、共通で使うフィクスチャとかを定義したい時に使うのがconftest.py
」
おわりに
最後までお読みいただき誠にありがとうございました!
pytestはめちゃくちゃ簡単にPythonでテストコードを書ける便利ツールとなっています。
みなさんもぜひpytestを学び、Pythonでテストコードをゴリゴリ書いていき、信頼性の高いコードを書いていきましょう!
それではまた〜🐶
🐶「ありがとうございました〜〜」