Pytestにおけるpytest-mockを使った外部依存関係のシミュレーション
Wenhao Wang
Dev Intern · Leapcell

はじめに
現代のソフトウェア開発では、アプリケーションが孤立して動作することはめったにありません。REST API、サードパーティライブラリ、データベースなどの外部サービスと頻繁にやり取りします。これらの依存関係はアプリケーションの機能にとって不可欠ですが、単体テストや統合テストに大きな課題をもたらします。テスト中に実際の外部システムに依存すると、テストが遅くなったり、ネットワークの問題やサービス障害による不安定さが発生したり、API使用料が発生することさえあります。ここで「モッキング」という概念が登場します。これらの外部依存関係をシミュレートすることで、制御可能で予測可能で高速なテスト環境を作成できます。この記事では、強力なPytestプラグインであるpytest-mockを活用して、外部APIやデータベースの呼び出しを効果的にモックし、Pythonアプリケーションの堅牢で効率的なテストを確保する方法を探ります。
コアコンセプトの理解
pytest-mockの実装に飛び込む前に、いくつかの基本的な概念を明確にしましょう。
- モッキング(Mocking): テストにおいて、モッキングとは、実際のオブジェクトや関数を、実際のオブジェクトの動作をシミュレートする代替物で置き換えることを指します。この代替物は「モックオブジェクト」または単に「モック」と呼ばれ、実際のコードを呼び出すことなく、返り値の制御、例外の発生、やり取りの監視を可能にします。
- スタビング(Stubbing): モッキングと交換可能に使用されることがよくありますが、スタビングは具体的には、テスト中に発生したメソッド呼び出しに対して事前にプログラムされた応答を提供することを指します。スタブの主な目的は、テスト中に発生した呼び出しに、動作を検証することではなく、決まった回答を返すことです。
- スパイ(Spying): スパイは、実際のパフォーマンスを維持しながら、実際のオブジェクトや関数をラップすることを含みます。スパイは、ラップされたオブジェクトとのやり取りを記録し、特定のメソッドが特定の引数で呼び出されたことをアサートすることを可能にします。pytest-mockは主にモッキングに焦点を当てていますが、その機能はやり取りを観察するためにも拡張できます。
- 単体テスト(Unit Testing): ソフトウェアアプリケーションの個々のユニットまたはコンポーネントを隔離してテストすることに焦点を当てます。モッキングは、テスト対象のユニットを依存関係から分離するために、ここで重要になります。
- 統合テスト(Integration Testing): アプリケーションのさまざまなユニットまたはコンポーネント間のやり取りを検証します。統合テストでは一部の実際の依存関係が含まれる場合がありますが、非常に不安定またはコストのかかる外部サービスに対してモッキングを使用することもできます。
pytest-mockは、Pythonの組み込みunittest.mockライブラリの便利なラッパーを提供するPytestフィクスチャです。Pytestの強力なテストフレームワークとシームレスに統合され、テスト内でモックオブジェクトを管理するためのクリーンで直感的な方法を提供します。
pytest-mockを使用した外部依存関係のシミュレーション
pytest-mockを使用する際の中心的な原則は、外部サービスとやり取りする実際のオブジェクトまたは関数を、制御されたモックオブジェクトに置き換えることです。これは通常、pytest-mockフィクスチャによって提供されるmocker.patch()メソッドを使用して行われます。
実用的な例を考えてみましょう。リモートAPIからユーザーデータを取得するPython関数と、データベースにデータを保存する別の関数があるとします。
アプリケーションコード(例:app.py):
import requests import sqlite3 class User: def __init__(self, user_id, name, email): self.user_id = user_id self.name = name self.email = email def __repr__(self): return f"User(id={self.user_id}, name={self.name}, email={self.email})" def fetch_user_from_api(user_id): """プレースホルダーAPIからユーザーデータを取得します。""" api_url = f"https://jsonplaceholder.typicode.com/users/{user_id}" try: response = requests.get(api_url) response.raise_for_status() # 不正なステータスコードに例外を発生させます data = response.json() return User(data['id'], data['name'], data['email']) except requests.exceptions.RequestException as e: print(f"ユーザー取得エラー:{e}") return None def save_user_to_db(user): """ユーザーデータをSQLiteデータベースに保存します。""" conn = None try: conn = sqlite3.connect('users.db') cursor = conn.cursor() cursor.execute(''' CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY, name TEXT NOT NULL, email TEXT NOT NULL ) ''') cursor.execute("INSERT INTO users (id, name, email) VALUES (?, ?, ?)", (user.user_id, user.name, user.email)) conn.commit() return True except sqlite3.Error as e: print(f"DBへのユーザー保存エラー:{e}") return False finally: if conn: conn.close() def get_and_save_user(user_id): """ユーザーを取得してデータベースに保存します。""" user = fetch_user_from_api(user_id) if user: return save_user_to_db(user) return False
次に、jsonplaceholder.typicode.comや実際のusers.dbファイルに実際にアクセスせずに、get_and_save_userのテストを記述しましょう。
pytest-mockを使用したテスト(例:test_app.py):
まず、pytestとpytest-mockをインストールしたことを確認してください。
pip install pytest pytest-mock requests
import pytest from unittest.mock import MagicMock import app # アプリケーションコード # テストケース1:正常な取得と保存 def test_get_and_save_user_success(mocker): # requests.getメソッドをモックします mock_response = MagicMock() mock_response.status_code = 200 mock_response.json.return_value = { 'id': 1, 'name': 'Leanne Graham', 'email': 'sincere@april.biz' } mocker.patch('requests.get', return_value=mock_response) # sqlite3.connectメソッドとその関連カーソル関数をモックします mock_conn = MagicMock() # commitとcloseが呼び出し可能であることを確認しますが、モックでは何も実行しません mock_conn.commit.return_value = None mock_conn.close.return_value = None mock_conn.cursor.return_value.execute.return_value = None # CREATE TABLEとINSERT用 mocker.patch('sqlite3.connect', return_value=mock_conn) # テスト対象の関数を呼び出します result = app.get_and_save_user(1) # アサート assert result is True # requests.getが呼び出されたことを検証します mocker.patch.call_args_list[0].assert_called_once_with('https://jsonplaceholder.typicode.com/users/1') # sqlite3.connectが呼び出されたことを検証します mock_conn.cursor.assert_called_once() mock_conn.cursor.return_value.execute.assert_any_call( ''' CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY, name TEXT NOT NULL, email TEXT NOT NULL ) ''' ) mock_conn.cursor.return_value.execute.assert_any_call( "INSERT INTO users (id, name, email) VALUES (?, ?, ?)", (1, 'Leanne Graham', 'sincere@april.biz') ) mock_conn.commit.assert_called_once() mock_conn.close.assert_called_once() # テストケース2:API呼び出し失敗(例:ネットワークエラー) def test_get_and_save_user_api_failure(mocker): # requests.getを例外を発生させるようにモックします mocker.patch('requests.get', side_effect=requests.exceptions.RequestException("Network error")) mocker.patch('sqlite3.connect') # APIが失敗しても、DBが触られないことを保証します result = app.get_and_save_user(1) assert result is False mocker.patch.call_args_list[0].assert_called_once_with('https://jsonplaceholder.typicode.com/users/1') # sqlite3.connectが呼び出されなかったことをアサートします app.sqlite3.connect.assert_not_called() # テストケース3:データベース保存失敗 def test_get_and_save_user_db_failure(mocker): # 正常なAPIレスポンスをモックします mock_response = MagicMock() mock_response.status_code = 200 mock_response.json.return_value = { 'id': 1, 'name': 'Leanne Graham', 'email': 'sincere@april.biz' } mocker.patch('requests.get', return_value=mock_response) # sqlite3.connectをコミット中に例外を発生させるようにモックします mock_conn = MagicMock() mock_conn.cursor.return_value.execute.return_value = None mock_conn.commit.side_effect = sqlite3.Error("Database write error") mocker.patch('sqlite3.connect', return_value=mock_conn) result = app.get_and_save_user(1) assert result is False mocker.patch.call_args_list[0].assert_called_once_with('https://jsonplaceholder.typicode.com/users/1') # DB接続とコミットの試行を検証します mock_conn.cursor.assert_called_once() mock_conn.commit.assert_called_once() mock_conn.close.assert_called_once() # エラー時でも接続が閉じられることを保証します
コード例の説明:
- mockerフィクスチャ:- pytest-mockは- mockerフィクスチャを提供し、モックオブジェクトのセットアップとクリーンアップを自動的に処理します。テストが終了すると、- mockerで作成されたすべてのパッチは自動的に元に戻り、テストの汚染を防ぎます。
- mocker.patch('module.object', ...): これはモックの主なメソッドです。- 最初の引数('requests.get')は、モックしたいオブジェクトの完全修飾パスを表す文字列です。定義された場所ではなく、オブジェクトがルックアップされる場所をパッチすることが重要です。app.pyではrequests.getが直接呼び出されるため、requests.getをパッチします。
- データベースの場合、sqlite3.connectが呼び出されます。初期モックによって返されるオブジェクトのモックを連鎖させることが重要です。たとえば、mock_conn.cursor.return_value.executeは、connectが返すcursorオブジェクトのexecuteメソッドをモックすることを可能にします。
 
- 最初の引数(
- return_value: モックオブジェクトのこの属性は、呼び出されたときに何を返すかを指定します。- mock_response.jsonの場合、APIのJSONレスポンスを模倣する辞書を- return_valueに設定します。
- side_effect:- return_valueの代わりに、- side_effectは、モックが呼び出されたときに例外を発生させるか、関数を呼び出すことができます。これは、- test_get_and_save_user_api_failureおよび- test_get_and_save_user_db_failureで示されているように、エラー条件をシミュレートするのに役立ちます。
- MagicMock: これは- unittest.mockの用途の広いモッククラスであり、アクセスされると属性やメソッドを自動的に作成するオブジェクトを作成します。これは、すべての属性やメソッドを気にする必要がないHTTPレスポンスやデータベース接続のような複雑なオブジェクトをシミュレートするのに非常に役立ちます。
- モックでのアサート: モックされた関数を呼び出した後、モックオブジェクトによって提供されるアサートメソッドを使用して、その動作を検証できます。
- mock.assert_called_once_with(...): モックが特定の引数でちょうど1回呼び出されたことをアサートします。
- mock.assert_any_call(...): モックが少なくとも1回特定の引数で呼び出されたことをアサートします。
- mock.assert_not_called(): モックが一度も呼び出されなかったことをアサートします。
- mock.call_args_list: モックへのすべての呼び出しのリストを提供します。
 
高度なシナリオとベストプラクティス
- クラスのパッチ: モックインスタンスを返すようにクラス全体をパッチできます。たとえば、mocker.patch('app.User', return_value=MagicMock())とすると、app.User()はMagicMockオブジェクトを返します。
- コンテキストマネージャー: withステートメント内で詳細なモックを行うために、mocker.patch.object()を使用できます。これは、一時的なパッチを適用するためにwithブロック内でよく使用されます。
- フィクスチャスコープ: Pytestのフィクスチャスコープを忘れないでください。mockerは通常関数スコープであり、各テスト後にパッチがクリーンアップされることを意味します。より永続的なモック(例:モジュール全体)が必要な場合は、mockerを使用したフィクスチャでautouse=Trueを設定するか、よりグローバルに構成することを検討してください。ただし、関数スコープのモックが一般的に推奨されます。
- モックするタイミングとしないタイミング: モッキングは、外部の予測不可能またはコストのかかる依存関係に最適です。内部の安定したコードでは、統合の問題を検出するために実際のデータでテストする方が良い場合が多いです。
- 過剰なモッキング: アプリケーションの過剰なモッキングに注意してください。モックしすぎるとテストはパスするかもしれませんが、モックが実際の動作を正確に反映していなかったり、ロジックではなくモック自体をテストしている可能性があるため、実際のアプリケーションで問題が発生する可能性があります。
結論
pytest-mockは、テスト中にPythonコードを外部依存関係から分離するためのエレガントで効果的なソリューションを提供します。mocker.patch()を戦略的に使用する方法と、unittest.mockオブジェクトの機能を理解することで、テストスイートの速度、信頼性、保守性を大幅に向上させることができます。モッキングを受け入れることで、テストは検査対象のロジックだけに集中でき、より堅牢で高品質なソフトウェアにつながります。依存関係のシミュレーションを効果的に行うことは、弾力性があり効率的なテストを書くための鍵となります。