🧪

pytest-mockを調査してわかったこと ~モックの最適な使い方~

2024/10/13に公開

はじめに

pytest でモックを使用する際に、毎回方法を調べ直すことが多いと感じたので、ここで自分なりのベストプラクティスをまとめ、振り返られるように整理しました。

この記事の内容には間違いや改善できる点があるかもしれません。もし気づいたことがあれば、ぜひフィードバックをいただけると嬉しいです。

この記事の対象者

  • pytest でモック化する時に悩む人

TL;DR

  1. マジックメソッドを使用したい -> MagicMock を使用
  2. 1以外 -> pytest-mock プラグインの mocker オブジェクトを使用

https://github.com/pytest-dev/pytest-mock

実装例(MagicMock)

1. __getitem__(インデックスアクセス)のモック

from unittest.mock import MagicMock

def test_magic_getitem():
    mock_obj = MagicMock()
    mock_obj.__getitem__.return_value = 'mocked value'

    # インデックスアクセスのテスト
    assert mock_obj['key'] == 'mocked value'

2. __setitem__(インデックス設定)のモック

from unittest.mock import MagicMock

def test_magic_setitem():
    mock_obj = MagicMock()
    mock_obj.__setitem__.side_effect = lambda key, value: print(f"{key} set to {value}")

    # インデックス設定のテスト
    mock_obj['key'] = 'value'
    mock_obj.__setitem__.assert_called_once_with('key', 'value')

3. __call__(関数呼び出し)のモック

from unittest.mock import MagicMock

def test_magic_call():
    mock_func = MagicMock()
    mock_func.return_value = 'called value'

    # 関数呼び出しのテスト
    assert mock_func(10) == 'called value'
    mock_func.assert_called_once_with(10)

4. __iter__(イテレータ)のモック

from unittest.mock import MagicMock

def test_magic_iter():
    mock_obj = MagicMock()
    mock_obj.__iter__.return_value = iter([1, 2, 3])

    # イテレータとしての動作をテスト
    assert list(mock_obj) == [1, 2, 3]

5. __enter____exit__(コンテキストマネージャ)のモック

from unittest.mock import MagicMock

def test_magic_context_manager():
    mock_obj = MagicMock()
    mock_obj.__enter__.return_value = 'entered'
    mock_obj.__exit__.return_value = False

    # with文でのテスト
    with mock_obj as result:
        assert result == 'entered'
    mock_obj.__enter__.assert_called_once()
    mock_obj.__exit__.assert_called_once()

6. __len__(オブジェクトの長さ)のモック

from unittest.mock import MagicMock

def test_magic_len():
    mock_obj = MagicMock()
    mock_obj.__len__.return_value = 10

    # 長さのテスト
    assert len(mock_obj) == 10
    mock_obj.__len__.assert_called_once()

7. __eq__(等価比較)のモック

from unittest.mock import MagicMock

def test_magic_eq():
    mock_obj = MagicMock()
    mock_obj.__eq__.return_value = True

    # 等価比較のテスト
    assert mock_obj == 42
    mock_obj.__eq__.assert_called_once_with(42)

8. __str__(文字列化)のモック

from unittest.mock import MagicMock

def test_magic_str():
    mock_obj = MagicMock()
    mock_obj.__str__.return_value = 'mocked string'

    # 文字列化のテスト
    assert str(mock_obj) == 'mocked string'
実装例(pytest-mock:mocker)

1. 関数レベルでのモック

def test_function_call(mocker):
    mock_func = mocker.patch('myapp.module.my_function')
    mock_func.return_value = 42
    result = another_function()
    mock_func.assert_called_once()
    assert result == 42

2. メソッドレベルでのモック

def test_class_method(mocker):
    mock_method = mocker.patch('myapp.module.MyClass.my_method')
    mock_method.return_value = 'mocked!'
    instance = MyClass()
    result = instance.my_method()
    assert result == 'mocked!'

3. クラス全体のモック

def test_class_instance(mocker):
    mock_class = mocker.patch('myapp.module.MyClass')
    mock_class.return_value.my_method.return_value = 'mocked!'
    result = some_function_using_class()
    assert result == 'mocked!'

4. モジュール全体のモック

モジュールレベルでモックすると、そのモジュール内のすべての関数やクラスがモックされます。

def test_module_level(mocker):
    mock_module = mocker.patch('myapp.module')
    mock_module.some_function.return_value = 'mocked!'
    result = another_function_using_module()
    assert result == 'mocked!'

5. プロパティのモック

def test_class_property(mocker):
    mocker.patch('myapp.module.MyClass.my_property', new_callable=mocker.PropertyMock, return_value='mocked!')
    instance = MyClass()
    assert instance.my_property == 'mocked!'

6. 辞書のモック

def test_dictionary_mock(mocker):
    mock_dict = mocker.patch('myapp.module.config', {'key': 'mocked_value'})
    assert my_function_using_config() == 'mocked_value'

7. コンストラクタのモック

クラスのインスタンス生成時の挙動をモックして、実際の処理を回避します。

def test_constructor_mock(mocker):
    mocker.patch('myapp.module.MyClass.__init__', return_value=None)
    instance = MyClass()
    assert instance is not None

8. コンテキストマネージャのモック

with ステートメントで使用するコンテキストマネージャをモックし、ファイル操作やトランザクション管理を再現します。

def test_context_manager(mocker):
    mock_open = mocker.patch('builtins.open', mocker.mock_open(read_data='mocked content'))
    with open('somefile.txt') as f:
        content = f.read()
    assert content == 'mocked content'

9. 例外のモック

特定の関数やメソッドで例外を発生させ、エラーハンドリングをテストします。

def test_exception_mock(mocker):
    mock_func = mocker.patch('myapp.module.my_function')
    mock_func.side_effect = Exception('mocked exception')
    with pytest.raises(Exception, match='mocked exception'):
        another_function()

10. 非同期関数のモック

非同期関数をモックして、即座に結果が返るように設定します。

@pytest.mark.asyncio
async def test_async_function(mocker):
    mock_func = mocker.patch('myapp.module.async_function')
    mock_func.return_value = 'mocked!'
    result = await async_function()
    assert result == 'mocked!'

モック化の手法

  1. Mock/MagicMockを使用
  2. with patchを使用
  3. mockerを使用
Mock/MagicMock 使用例
from unittest.mock import MagicMock
import pytest

class APIClient:
    def get_data(self):
        pass

def fetch_data(api_client):
    return api_client.get_data()

def test_fetch_data():
    api_client_mock = MagicMock()
    api_client_mock.get_data.return_value = {'data': 'value'}
    result = fetch_data(api_client_mock)

    assert result == {'data': 'value'}
with patch 使用例
from unittest.mock import patch

class APIClient:
    def get_data(self):
        pass

def fetch_data(api_client):
    return api_client.get_data()

def test_fetch_data():
    with patch(__name__ + ".APIClient", autospec=True) as mock_client:
        mock_client.get_data.return_value = {'data': 'value'}
    result = fetch_data(mock_client)

    assert result == {'data': 'value'}
mocker使用例

class APIClient:
    def get_data(self):
        pass

def fetch_data(api_client):
    return api_client.get_data()

def test_with_mocker(mocker):
    mock_api_client = mocker.patch('myapp.api.client.APIClient')
    mock_api_client.return_value.get_data.return_value = {'data': 'result'}
    result = fetch_data(mock_api_client)
    assert result == {'data': 'result'}

比較表

ライブラリ スコープ管理 メソッドシグネチャの引数チェック 記載方法が簡潔 マジックメソッドの有無
Mock/MagicMock ✖︎ ✖︎
with patch ✖︎
mocker ✖︎

スコープ管理

スコープ管理が適切にできておらず失敗する例
import pytest
from unittest.mock import MagicMock, patch

class ExternalAPIClient:
    def get(self, url):
        pass

def get_user_info(user_id):
    response = ExternalAPIClient().get(f"https://api.example.com/users/{user_id}")
    if response.status_code == 200:
        return response.json()
    return None

@pytest.fixture
def mock_api():
    mock_client = MagicMock()
    patcher = patch('myapp.services.ExternalAPIClient', return_value=mock_client)
    patcher.start()
    yield mock_client
    # patcher.stop() を呼び忘れている

def test_get_user_info_success(mock_api):
    mock_api.get.return_value.status_code = 200
    mock_api.get.return_value.json.return_value = {'user_id': 1, 'name': 'John Doe'}

    result = get_user_info(1)
    assert result == {'user_id': 1, 'name': 'John Doe'}

def test_get_user_info_failure(mock_api):
    result = get_user_info(2)
    assert result is None  # ここで失敗するはずだが、前のモックが残っているため成功してしまう
mocker を使っておりテスト毎のスコープ管理ができている例
import pytest

class ExternalAPIClient:
    def get(self, url):
        pass

def get_user_info(user_id):
    response = ExternalAPIClient().get(f"https://api.example.com/users/{user_id}")
    if response.status_code == 200:
        return response.json()
    return None

@pytest.fixture
def mock_api(mocker):
    return mocker.patch('myapp.services.ExternalAPIClient')

# テスト1: ユーザーが正常に取得できるケース
def test_get_user_info_success(mock_api):
    mock_api.return_value.get.return_value.status_code = 200
    mock_api.return_value.get.return_value.json.return_value = {'user_id': 1, 'name': 'John Doe'}

    result = get_user_info(1)
    assert result == {'user_id': 1, 'name': 'John Doe'}

def test_get_user_info_failure(mock_api):
    mock_api.return_value.get.return_value.status_code = 404
    result = get_user_info(2)
    assert result is None

メソッドシグネチャの引数チェック(spec / autospec)

spec
from unittest.mock import MagicMock

class APIClient:
    def get_data(self):
        pass

mock_client = MagicMock(spec=APIClient)
mock_client.get_data.return_value = {'data': 'value'}

# 存在しないメソッドにアクセスしようとするとエラーになる
mock_client.non_existing_method()  # AttributeError: Mock object has no attribute 'non_existing_method'
from unittest.mock import MagicMock

class APIClient:
    def get_data(self):
        pass

# spec を使用した例
mock_client = MagicMock(spec=APIClient)
mock_client.get_data.return_value = {'data': 'value'}

mock_client.get_data("test1", "test2")  # 引数のシグネチャが模倣されていないので、エラーとならない
autospec
from unittest.mock import patch

class APIClient:
    def get_data(self, param):
        pass

def fetch_data(api_client):
    return api_client.get_data('test')

def test_fetch_data():
    with patch('__main__.APIClient', autospec=True) as mock_client:
        mock_client.get_data.return_value = {'data': 'value'}
        result = fetch_data(mock_client)
        assert result == {'data': 'value'}

        # 引数のシグネチャも模倣されているので、間違った引数で呼び出すとエラーになる
        mock_client.get_data()  # TypeError: missing 1 required positional argument: 'param'
from unittest.mock import patch

class APIClient:
    def get_data(self, param):
        pass

def fetch_data(api_client):
    return api_client.get_data('test')

def test_fetch_data(mocker):
    mock_client = mocker.patch('__main__.APIClient', autospec=True)
    mock_client.get_data.return_value = {'data': 'value'}

    # 呼び出しは正常に動作する
    result = fetch_data(mock_client)
    assert result == {'data': 'value'}

    # 引数のシグネチャも模倣されているので、間違った引数で呼び出すとエラーになる
    mock_client.get_data()  # TypeError: missing 1 required positional argument: 'param'

番外編 mocker は裏側で unittest.mock.patch と unittest.mock.MagicMock を使用している単なるラッパー

mocker が使われる時、実際には unittest.mock.patchMagicMock が裏側で動いています。ここではその動作を追ってみます。

https://github.com/pytest-dev/pytest-mock

1. `_mocker` 関数での `MockerFixture` インスタンス生成

_mocker 関数で MockerFixture のインスタンスが作成され、その結果が yield によってテスト関数に渡されます。
この yield で渡された MockerFixture インスタンスが mocker としてテスト関数内で使われることになります。

src/pytest_mock/plugin.py
def _mocker(pytestconfig: Any) -> Generator[MockerFixture, None, None]:
    """
    Return an object that has the same interface to the `mock` module, but
    takes care of automatically undoing all patches after each test method.
    """
    result = MockerFixture(pytestconfig)
    yield result
    result.stopall()


mocker = pytest.fixture()(_mocker)  # default scope is function
class_mocker = pytest.fixture(scope="class")(_mocker)
module_mocker = pytest.fixture(scope="module")(_mocker)
package_mocker = pytest.fixture(scope="package")(_mocker)
session_mocker = pytest.fixture(scope="session")(_mocker)
2. `mocker.patch` の呼び出し

mockerMockerFixture のインスタンスであり、mocker.patch が呼び出されると、MockerFixture クラス内の patch メソッドが実行されます。このメソッドは、内部で _Patcher クラスを使って実際のパッチ処理を行います。

src/pytest_mock/plugin.py
class MockerFixture:
    """
    Fixture that provides the same interface to functions in the mock module,
    ensuring that they are uninstalled at the end of each test.
    """

    def __init__(self, config: Any) -> None:
        self._mock_cache: MockCache = MockCache()
        self.mock_module = mock_module = get_mock_module(config)
        self.patch = self._Patcher(self._mock_cache, mock_module)  # type: MockerFixture._Patcher

self.mock_module はデフォルトでは unittest.mock となります。

src/pytest_mock/_util.py
def get_mock_module(config):
    """
    Import and return the actual "mock" module. By default this is
    "unittest.mock", but the user can force to always use "mock" using
    the mock_use_standalone_module ini option.
    """
    global _mock_module
    if _mock_module is None:
        use_standalone_module = parse_ini_boolean(
            config.getini("mock_use_standalone_module")
        )
        if use_standalone_module:
            import mock

            _mock_module = mock
        else:
            import unittest.mock

            _mock_module = unittest.mock

    return _mock_module


def parse_ini_boolean(value: Union[bool, str]) -> bool:
    if isinstance(value, bool):
        return value
    if value.lower() == "true":
        return True
    if value.lower() == "false":
        return False
    raise ValueError("unknown string for bool: %r" % value)
3. `_Patcher` クラスによるパッチ処理

_Patcher クラスは、unittest.mock.patchunittest.mock.patch.object のようなモック機能をラップするためのクラスです。mocker.patch の呼び出しは、_Patcher.call メソッドを通じて処理されます。

src/pytest_mock/plugin.py
    class _Patcher:
        """
        Object to provide the same interface as mock.patch, mock.patch.object,
        etc. We need this indirection to keep the same API of the mock package.
        """

        DEFAULT = object()

        def __init__(self, mock_cache, mock_module):
            self.__mock_cache = mock_cache
            self.mock_module = mock_module

        .
        .
        .

        def __call__(
            self,
            target: str,
            new: builtins.object = DEFAULT,
            spec: Optional[builtins.object] = None,
            create: bool = False,
            spec_set: Optional[builtins.object] = None,
            autospec: Optional[builtins.object] = None,
            new_callable: Optional[Callable[[], Any]] = None,
            **kwargs: Any,
        ) -> Any:
            """API to mock.patch"""
            if new is self.DEFAULT:
                new = self.mock_module.DEFAULT
            return self._start_patch(
                self.mock_module.patch,
                True,
                target,
                new=new,
                spec=spec,
                create=create,
                spec_set=spec_set,
                autospec=autospec,
                new_callable=new_callable,
                **kwargs,
            )
4. `_start_patch` メソッドでのパッチの開始

ここでは、unittest.mock.patch が実際に呼び出され、その結果としてモックされたオブジェクト(MagicMock)が返されます。

p.start(): patch() によって作成された Mock オブジェクトが開始され、モックされたオブジェクト(MagicMock)が返されます。
self.__mock_cache.add(): モックがキャッシュに追加され、テスト終了時にクリーンアップされるようになります。

src/pytest_mock/plugin.py
        def _start_patch(
            self, mock_func: Any, warn_on_mock_enter: bool, *args: Any, **kwargs: Any
        ) -> MockType:
            """Patches something by calling the given function from the mock
            module, registering the patch to stop it later and returns the
            mock object resulting from the mock call.
            """
            p = mock_func(*args, **kwargs)
            mocked: MockType = p.start()
            self.__mock_cache.add(mock=mocked, patch=p)
            if hasattr(mocked, "reset_mock"):
                # check if `mocked` is actually a mock object, as depending on autospec or target
                # parameters `mocked` can be anything
                if hasattr(mocked, "__enter__") and warn_on_mock_enter:
                    mocked.__enter__.side_effect = lambda: warnings.warn(
                        "Mocks returned by pytest-mock do not need to be used as context managers. "
                        "The mocker fixture automatically undoes mocking at the end of a test. "
                        "This warning can be ignored if it was triggered by mocking a context manager. "
                        "https://pytest-mock.readthedocs.io/en/latest/remarks.html#usage-as-context-manager",
                        PytestMockWarning,
                        stacklevel=5,
                    )
            return mocked
5. **テスト関数でのモック使用**

テスト関数で mocker.patch が呼び出されると、MagicMock オブジェクトが返され、テスト内でそのモックが使用されます。

def test_example(mocker):
    mock_func = mocker.patch('myapp.module.some_function')
    mock_func.return_value = 'mocked_value'
    result = some_function()  # モックされた関数が呼ばれる
    assert result == 'mocked_value'
6.テスト終了後のクリーンアップ

テストが終了すると、_mocker 関数の result.stopall() によって、全てのモックが解除されます。stopall() メソッドは、キャッシュされていたすべてのモックを解除します。

番外編 Mock と MagicMock の違い

MagicMock は Python の マジックメソッド(getitemsetitemcall など) に対応しており、これらをモックする際に便利。一方、Mock クラスにはこれらのマジックメソッドがデフォルトでは備わってない。

from unittest.mock import Mock, MagicMock

# Mock ではマジックメソッドはデフォルトでモックされない
mock_obj = Mock()
try:
    mock_obj['key']  # TypeError: 'Mock' object is not subscriptable
except TypeError as e:
    print(f"Mock: {e}")

# MagicMock ではマジックメソッドが自動でモックされる
magic_mock_obj = MagicMock()
magic_mock_obj['key'] = 'value'
print(magic_mock_obj['key'])  # 'value'

# MagicMock では __call__ が自動モックされる
magic_mock_func = MagicMock()
magic_mock_func(1, 2)
magic_mock_func.assert_called_once_with(1, 2)  # 正常に通る

# Mock では __call__ がサポートされないため TypeError が発生
mock_func = Mock()
try:
    mock_func(1, 2)  # TypeError: 'Mock' object is not callable
except TypeError as e:
    print(f"Mock: {e}")
# MagicMock の __iter__ を使ってループ処理をモックできる。
magic_mock_iter = MagicMock()
magic_mock_iter.__iter__.return_value = iter([1, 2, 3])

for item in magic_mock_iter:
    print(item)  # 1, 2, 3 と出力される

番外編 Mocker を fixture で使用する

共通化と再利用を目的として mocker.patchfixture を組み合わせることで、モックの設定を共通化し、複数のテストケースで再利用できます。

# myapp/services.py
from myapp.cache import CacheSystem
from myapp.database import db_client
from myapp.api.client import APIClient

# サービス関数: キャッシュ、DB、APIの利用
def complex_service():
    cache = CacheSystem()
    cached_data = cache.get('some_key')

    if cached_data:
        return cached_data

    db_data = db_client().fetch_data()
    api_data = APIClient().get_data()
    result = {**db_data, **api_data}
    return result


# テストコード: キャッシュ、DB、APIをモックしてテスト
import pytest

@pytest.fixture
def mock_cache_with_hit(mocker):
    mock_cache = mocker.patch('myapp.cache.CacheSystem')
    mock_cache.return_value.get.return_value = {'cached_data': 'hit'}
    return mock_cache

@pytest.fixture
def mock_cache_without_hit(mocker):
    mock_cache = mocker.patch('myapp.cache.CacheSystem')
    mock_cache.return_value.get.return_value = None
    return mock_cache

@pytest.fixture
def mock_db_client(mocker):
    mock_client = mocker.patch('myapp.database.db_client')
    mock_client.return_value.fetch_data.return_value = {'db_data': 'value'}
    return mock_client

@pytest.fixture
def mock_api_client(mocker):
    mock_client = mocker.patch('myapp.api.client.APIClient')
    mock_client.return_value.get_data.return_value = {'api_data': 'value'}
    return mock_client

# キャッシュがヒットする場合のテストケース
def test_complex_service_cache_hit(mock_cache_with_hit, mock_db_client, mock_api_client):
    result = complex_service()
    assert result == {'cached_data': 'hit'}
    mock_db_client.return_value.fetch_data.assert_not_called()
    mock_api_client.return_value.get_data.assert_not_called()

# キャッシュがヒットしない場合のテストケース
def test_complex_service_cache_miss(mock_cache_without_hit, mock_db_client, mock_api_client):
    result = complex_service()
    assert result == {'db_data': 'value', 'api_data': 'value'}
    mock_db_client.return_value.fetch_data.assert_called_once()
    mock_api_client.return_value.get_data.assert_called_once()

番外編 pytest-mock が pytest のプラグインとして取り込まれる仕組み

pytest-mockpytest のプラグインとして機能するための仕組みについて調べてみます。

1. pytest-mock が pytest プラグインとして読み込まれる

pytest には、プラグインを自動的に検出して読み込むメカニズムがあり、pytest-mock は、この仕組みを利用している。
これは、setup.pypyproject.tomlentry_points セクションで pytest11 というエントリーポイントを定義することによって行われるようです。以下がおそらく pyproject.toml の該当部分

[project.entry-points.pytest11]
 pytest_mock = "pytest_mock"

このエントリーポイントにより、pytestpytest-mock をプラグインとして認識されるらしい。

2. プラグインとしてのフック関数が認識される

pytest_configurepytest_addoption といったフック関数を実装することで、pytest の動作にフックして特定の動作を追加。

2.1 pytest_addoption

pytest_addoption は、pytest に追加のコマンドラインオプションや設定を追加するために使われるとのこと。pytest-mock では、以下のように新しいオプションを追加してあります。

def pytest_addoption(parser: Any) -> None:
    parser.addini(
        "mock_traceback_monkeypatch",
        "Monkeypatch the mock library to improve reporting of the "
        "assert_called_... methods",
        default=True,
    )
    parser.addini(
        "mock_use_standalone_module",
        'Use standalone "mock" (from PyPI) instead of builtin "unittest.mock" '
        "on Python 3",
        default=False,
    )
  • parser.addini を使って、pytestmock_traceback_monkeypatchmock_use_standalone_module という設定を追加しています。これにより、ユーザーは pytest.ini ファイルでこれらの設定を変更できます。

2.2 pytest_configure

pytest_configure は、pytest が初期化されるときに呼ばれ、プラグインの設定や初期化を行います。pytest-mock では、pytest_configure を使って、トレースバックのカスタマイズなどを行っているもよう。

def pytest_configure(config: Any) -> None:
    tb = config.getoption("--tb", default="auto")
    if (
        parse_ini_boolean(config.getini("mock_traceback_monkeypatch"))
        and tb != "native"
    ):
        wrap_assert_methods(config)

番外編 スコープ粒度で mocker を使用する

import pytest
from unittest.mock import MagicMock

class ExternalService:
    def fetch_data(self, identifier):
        return f"Real data for {identifier}"

service = ExternalService()

# クラススコープでモック
@pytest.fixture(scope="class")
def class_mocker(class_mocker):
    class_mocker.patch.object(service, 'fetch_data', return_value="Class Scoped Data")
    return class_mocker

# モジュールスコープでモック
@pytest.fixture(scope="module")
def module_mocker(module_mocker):
    module_mocker.patch.object(service, 'fetch_data', return_value="Module Scoped Data")
    return module_mocker

# パッケージスコープでモック
@pytest.fixture(scope="package")
def package_mocker(package_mocker):
    package_mocker.patch.object(service, 'fetch_data', return_value="Package Scoped Data")
    return package_mocker

# セッションスコープでモック
@pytest.fixture(scope="session")
def session_mocker(session_mocker):
    session_mocker.patch.object(service, 'fetch_data', return_value="Session Scoped Data")
    return session_mocker


# テストケース1: クラススコープのモックを確認
@pytest.mark.usefixtures("class_mocker")
class TestClassScope:
    def test_class_scope(self):
        assert service.fetch_data("id1") == "Class Scoped Data"

    def test_class_scope_again(self):
        assert service.fetch_data("id2") == "Class Scoped Data"

# テストケース2: モジュールスコープのモックを確認
@pytest.mark.usefixtures("module_mocker")
class TestModuleScope:
    def test_module_scope(self):
        assert service.fetch_data("id1") == "Module Scoped Data"

    def test_module_scope_again(self):
        assert service.fetch_data("id2") == "Module Scoped Data"

# テストケース3: パッケージスコープのモックを確認
@pytest.mark.usefixtures("package_mocker")
class TestPackageScope:
    def test_package_scope(self):
        assert service.fetch_data("id1") == "Package Scoped Data"

    def test_package_scope_again(self):
        assert service.fetch_data("id2") == "Package Scoped Data"

# テストケース4: セッションスコープのモックを確認
@pytest.mark.usefixtures("session_mocker")
class TestSessionScope:
    def test_session_scope(self):
        assert service.fetch_data("id1") == "Session Scoped Data"

    def test_session_scope_again(self):
        assert service.fetch_data("id2") == "Session Scoped Data"

Discussion