🙄

俺と一緒にpytestをゼロから理解しないかい?

に公開

こんにちは。もんた。です。

新しい試みとして、私がゼロベースから「完全に理解した」領域までのプロセスをそのまま記事にしたら「そのトピックを理解できる」かつ「記事いっぱい書いててなんかすごいやつ」という一石二鳥かませるんじゃね?これイノベーションじゃね?と思ったの始めてみます。

これはなに

この記事は私が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 との互換性 👍

  • 既存の unittestnose ベースのテストスイートを持っている場合でも、多くの場合、Pytestはそれらのテストをそのまま検出・実行できます。これにより、既存のテスト資産を活かしながら段階的にPytestへ移行することが可能です。

なぜPytestを選ぶのか? 🤔 (vs unittest)

Python標準の unittest と比較した場合、Pytestには以下のような利点があります。

  • 記述の簡潔性: ボイラープレートが少なく、より少ないコード量でテストを記述できます。
  • フィクスチャの優位性: unittestsetUp/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_プレフィックスをつけるだけで勝手にテストコードとして認識してくれるそうです。かなり楽ですね。

いくつか気になるキーワードが出てきましたね。

  1. 強力なフィクスチャ機能
  2. パラメータ化テスト
  3. 柔軟なテスト選択(マーカー)
  4. unittestとの互換性

これらに関しては後ほど詳しくみていくことにしましょう。

つまり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()

実行方法:

  1. 上記のコードをそれぞれ money_pydantic.pytest_money_pydantic.py という名前で、同じディレクトリに保存します。

  2. ターミナルを開き、これらのファイルが保存されているディレクトリに移動します。

  3. 以下のコマンドを実行します。

    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.fixtureデコレータについて

まずは@pytest.fixture の基本的な使い方を理解していきましょう!

Pytestフィクスチャの基本のキ! @pytest.fixture の使い方 ✨

Pytestのフィクスチャ (fixture) は、テストを実行するための**「準備」**を行うための機能です。

例えば、テストで使うデータを用意したり、テスト対象のオブジェクトを作成したりするのに使います。

なぜフィクスチャを使うの? 🤔

  • 準備コードの共通化: 同じような準備(セットアップ)を複数のテストで行う場合、その準備コードをフィクスチャとして一つにまとめることで、コードの重複をなくし、修正も楽になります (DRY原則)。
  • テストの見通し向上: テスト関数本体は「何をテストするか」というロジックに集中でき、準備のためのコードを分離できるため、テストが読みやすくなります。
  • 自動的な準備: テスト関数が必要な準備を、引数で指定するだけでPytestが自動的に実行してくれます。

フィクスチャの作り方 🛠️

  1. 準備用の関数を定義します。
  2. その関数の上に @pytest.fixture という目印(デコレータ)を付けます。
  3. 関数の中で必要な準備を行い、テスト関数で使いたい値(データやオブジェクトなど)を 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 # ← 準備したデータを返す

フィクスチャの使い方 🚀

  1. テストを書きたい関数(test_ で始まる関数)を定義します。
  2. その関数の引数に、使いたいフィクスチャの関数名をそのまま書きます。
  3. 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 の基本的な使い方はこれだけです!

  1. @pytest.fixture デコレータを使って準備用の関数を作る。
  2. テスト関数の引数にその関数名を書く。

これだけで、テストの準備コードを共通化し、テスト本体をシンプルに保つことができます。まずはこの基本を押さえて、テストコードを整理整頓してみましょう!

例えば、テストで使うデータを用意したり、テスト対象のオブジェクトを作成したりするのに使います。

ここが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を使うことで、事前に定義したオブジェクトを使いまわせるようになるんですね。

@pytest.mark.*デコレータについて

テストに目印を! @pytest.mark でテストを自在にコントロール 🏷️

@pytest.mark は、Pytest におけるマーキング機能の基本的な仕組みを提供するデコレータです。テスト関数やテストクラスに対して、様々な種類の**マーカー(目印や属性)**を付与するために使われます。

これにより、テストを分類したり、特定の条件下で実行を制御したり、テストの挙動自体を変更したりすることが可能になります。

なぜマーカーを使うの? 🤔

マーカーを使う主な目的は以下の通りです。

  1. テストの分類とグルーピング:
    • 特定の機能 (@pytest.mark.login, @pytest.mark.search)、テストの種類 (@pytest.mark.unit, @pytest.mark.integration, @pytest.mark.ui)、あるいは実行にかかる時間 (@pytest.mark.slow) など、自由な基準でテストを分類できます。
  2. テストの選択実行:
    • コマンドラインオプション m を使うことで、特定のマーカーが付いたテストだけを実行したり (pytest -m slow)、逆に除外したり (pytest -m "not slow") することができます。これにより、時間がない時に高速なテストだけを実行したり、特定の機能に関するテストだけを実行したりできます。
  3. テストの実行制御:
    • 特定の条件下でテストをスキップさせたり、失敗することを期待したりするマーカーがあります。
  4. テストへのパラメータ供給:
    • @pytest.mark.parametrize も実はマーカーの一種で、テスト関数に複数のデータセットを渡すという特別な役割を持っています。
  5. プラグインとの連携:
    • 多くの 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_authnegative の両方が付いたテストのみ実行 (1件)
  • pytest -m "user_auth or search": user_auth または search が付いたテストを実行 (3件)

2. テストのスキップ (skip, skipif)

  • @pytest.mark.skip(reason="理由"): テストを無条件にスキップします。
  • @pytest.mark.skipif(condition, reason="理由"): conditionTrue と評価された場合にテストをスキップします。

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_authslow のような独自のマーカーを使いましたが、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.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: 特定のパラメータセットにマーカー (例: xfailskipif) を適用したい場合に使います。

    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.mockでモックを使いこなそう

依存関係を断ち切る! pytest-mockmocker フィクスチャで快適モックテスト 🎭

テストを書いていると、テストしたいコードが外部のシステム(データベース、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_valueside_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_servicerequests.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.raisesmatch 引数に文字列や正規表現パターンを指定すると、発生した例外のエラーメッセージがそのパターンにマッチするかどうかを直接検証できます。こちらの方がコードがより簡潔になります。

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.raiseswithと一緒に使います。

基本的な使い方 📖

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です。

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不要で使えるファイルです。

おわりに

最後までお読みいただき誠にありがとうございました!

pytestはめちゃくちゃ簡単にPythonでテストコードを書ける便利ツールとなっています。

みなさんもぜひpytestを学び、Pythonでテストコードをゴリゴリ書いていき、信頼性の高いコードを書いていきましょう!

それではまた〜🐶

Discussion