unittest mockについて mockの理解から始める python 試験対策

Mockとは何か?

**Mock(モック)**は、ユニットテストにおいて、テスト対象のオブジェクトやメソッドの振る舞いを再現するための「偽物のオブジェクト」です。実際のオブジェクトを使うのが難しい、または不必要な場合に、あたかもそのオブジェクトが存在しているかのように動作させるための手法です。

特に、外部リソース(データベース、API、ファイルシステムなど)への依存を回避し、純粋にロジックのテストに集中するために使われます。

ポイント: 外部リソースをテストする場合にMockが便利です。しかし、外部リソースを使わない場合でも、Mockを使ってテストを行うことができます。

どういう場面で使うのか?

以下のような場面でMockを使います:

  1. 外部依存がある場合 テスト対象の関数やメソッドが外部APIを呼び出す場合、実際にAPIを呼び出すのではなく、Mockでその呼び出しを模倣し、レスポンスを手動で設定できます。

  2. 高コストな処理が含まれる場合 実際にデータベースやネットワーク通信が絡む処理は、時間がかかることがあります。そのような処理をテストする際、Mockを使って高速にテストを行うことができます。

  3. 状態をコントロールしたい場合 テスト対象のオブジェクトの状態や、依存するメソッドの返り値を任意に制御したい場合、Mockを使ってそのメソッドが必ず特定の値を返すようにできます。

なぜ使うのか?

  1. テストの独立性を保つ テストが外部リソースに依存する場合、そのリソースの状態によってテスト結果が左右されます。Mockを使うことで、外部依存を排除し、テスト対象のロジックにのみ集中できます。

  2. 高コストな処理を回避する 外部APIやデータベースの呼び出しが多いと、テスト実行に時間がかかります。Mockを使えば、時間を節約して効率的にテストを行うことができます。

  3. テストの再現性を保つ 外部リソース(例えば、外部APIやネットワーク)が不安定な場合、テストの再現性が失われます。Mockを使えば、テストが常に一定の状態で実行されるため、再現性が確保できます。

Mockを使うことで得られるメリット

高速なテスト実行: 外部リソースに依存しないため、テストの速度が向上します。

安定したテスト環境: ネットワークエラーや外部リソースの不具合に左右されず、安定したテストが行えます。

細かい制御が可能: 実際のオブジェクトが返すデータを細かく制御できるため、テストの境界条件やエッジケースを容易にシミュレートできます。

Mockを使わなかった場合と使った場合の比較

次に、実際のコードでMockを使わなかった場合と使った場合を比較してみます。

Mockを使わない場合

まず、Mockを使わずに外部APIを呼び出すテストを行います。ここでは、ユーザー情報を外部APIから取得するクラスをテストします。

# external_api.py (外部APIからデータを取得するクラス)
import requests

class UserAPI:
    def fetch_user(self, user_id: int) -> dict:
        response = requests.get(f"https://api.example.com/users/{user_id}")
        return response.json()
# test_user_api.py (Mockを使わないテスト)
import unittest
from external_api import UserAPI

class TestUserAPI(unittest.TestCase):
    def test_fetch_user(self):
        user_api = UserAPI()
        user = user_api.fetch_user(1)
        self.assertEqual(user['name'], 'Alice')

if __name__ == "__main__":
    unittest.main()

このテストでは、実際に外部APIにリクエストを送信してユーザーデータを取得しますが、次の問題があります:

APIがダウンしている場合、テストが失敗する。

APIの応答が遅いと、テストが遅くなる。

テスト中に実際のユーザーデータが変わる可能性がある。

Mockを使った場合

外部リソースをテストする場合、Mockを使って外部依存を排除できます。patchというデコレータとセットで使うことが多いです。

patchを使ったMockの適用

patchとは?

patchは、unittest.mockモジュールから提供されているデコレータで、特定のオブジェクトや関数を一時的にMockに置き換えるために使用されます。これにより、テスト中に本来呼び出されるはずの関数やオブジェクトをMockに差し替え、外部リソースへのアクセスを避けることができます。

patchの基本的な使い方

以下に、requests.getをMockに置き換える例を示します。

from unittest.mock import patch
import requests

def get_user_data():
    response = requests.get("https://api.example.com/user/1")
    return response.json()

テストコード:

from unittest.mock import patch
import unittest

class TestGetUserData(unittest.TestCase):
    @patch('requests.get')  # 'requests.get' をモックに置き換える
    def test_get_user_data(self, mock_get):
        # mock_getに対して返り値を指定
        mock_get.return_value.json.return_value = {"id": 1, "name": "Alice"}
        
        # 実際にget_user_dataを呼び出すと、モックが動作
        result = get_user_data()
        
        # 結果を確認
        self.assertEqual(result["name"], "Alice")

if __name__ == "__main__":
    unittest.main()

解説:

@patch('requests.get')を使って、requests.getをモックに置き換えています。 mock_get.return_value.json.return_valueで、response.json()が返す値を設定しています。 これにより、実際のAPIを呼び出さずにテストを行うことができます。 patchのターゲット指定のポイント patchで指定するターゲットは、実際にテスト対象がインポートしている場所を指定する必要があります。

例えば、external_api.pyでrequestsをインポートしている場合:

# external_api.py
import requests

class UserAPI:
    def fetch_user(self, user_id):
        return requests.get(f"https://api.example.com/users/{user_id}").json()

テストコードでは、external_api.requests.getをモックに置き換えます。

from unittest.mock import patch
import unittest
from external_api import UserAPI

class TestUserAPIWithMock(unittest.TestCase):
    @patch('external_api.requests.get')
    def test_fetch_user(self, mock_get):
        mock_get.return_value.json.return_value = {'id': 1, 'name': 'Alice'}
        user_api = UserAPI()
        user = user_api.fetch_user(1)
        self.assertEqual(user['name'], 'Alice')

if __name__ == "__main__":
    unittest.main()

Mockを使うメリット

APIがダウンしていてもテストが実行できる: 外部リソースに依存しないため、APIが利用できない状況でもテストが動作します。

テストの実行が高速化する: 実際のAPIリクエストを送信しないため、テストの実行速度が向上します。

再現性が高い: Mockで設定したデータを常に返すため、外部の状況に左右されず、再現性の高いテストが可能です。


patchのオプションと高度な使い方

unittest.mockのpatchには、いくつかのオプションや追加機能があり、テストの際に柔軟にモックを設定できます。以下は主なオプションや機能です。

side_effect

side_effectは、モックの動作を細かく制御するために使います。これにより、モックが呼ばれたときに例外を発生させたり、異なる結果を順番に返すことができます。

例1: 例外を発生させる

from unittest.mock import patch
import requests
import unittest

def get_user_data():
    response = requests.get("https://api.example.com/user/1")
    return response.json()

class TestGetUserData(unittest.TestCase):
    @patch('requests.get')
    def test_get_user_data_with_exception(self, mock_get):
        # 呼び出されたときに例外を発生させる
        mock_get.side_effect = requests.ConnectionError("Connection failed")
        
        with self.assertRaises(requests.ConnectionError) as context:
            get_user_data()
        self.assertEqual(str(context.exception), "Connection failed")

if __name__ == "__main__":
    unittest.main()

例2: 異なる値を順番に返す

from unittest.mock import patch
import unittest

def get_user_data():
    response = requests.get("https://api.example.com/user/1")
    return response.json()

class TestGetUserData(unittest.TestCase):
    @patch('requests.get')
    def test_get_user_data_multiple_calls(self, mock_get):
        # 1回目と2回目で異なる結果を返す
        mock_get.return_value.json.side_effect = [
            {'id': 1, 'name': 'Alice'},
            {'id': 2, 'name': 'Bob'}
        ]
        
        result1 = get_user_data()
        result2 = get_user_data()
        
        self.assertEqual(result1['name'], 'Alice')
        self.assertEqual(result2['name'], 'Bob')

if __name__ == "__main__":
    unittest.main()

return_value

return_valueは、モックされたメソッドや関数が呼び出されたときに常に返される値を設定します。

from unittest.mock import patch
import unittest

def get_user_data():
    response = requests.get("https://api.example.com/user/1")
    return response.json()

class TestGetUserData(unittest.TestCase):
    @patch('requests.get')
    def test_get_user_data(self, mock_get):
        # 常に同じ結果を返す
        mock_get.return_value.json.return_value = {'id': 1, 'name': 'Alice'}
        
        result = get_user_data()
        self.assertEqual(result['name'], 'Alice')

if __name__ == "__main__":
    unittest.main()

side_effectとreturn_valueの優先順位

side_effectとreturn_valueを同時に設定した場合、side_effectが優先されます。つまり、side_effectが設定されている場合、return_valueは無視されます。

from unittest.mock import patch
import requests
import unittest

def get_user_data():
    response = requests.get("https://api.example.com/user/1")
    return response.json()

class TestGetUserData(unittest.TestCase):
    @patch('requests.get')
    def test_side_effect_over_return_value(self, mock_get):
        # side_effectで例外を発生させる
        mock_get.side_effect = requests.ConnectionError("Connection failed")
        
        # return_valueで結果を設定(無視される)
        mock_get.return_value.json.return_value = {'id': 1, 'name': 'Alice'}
        
        with self.assertRaises(requests.ConnectionError) as context:
            get_user_data()
        self.assertEqual(str(context.exception), "Connection failed")

if __name__ == "__main__":
    unittest.main()

call_count と assert_called_with

モックが何回呼ばれたかや、どのような引数で呼ばれたかを確認するためのメソッドです。

from unittest.mock import patch
import unittest

def get_user_data():
    response = requests.get("https://api.example.com/user/1")
    return response.json()

class TestGetUserData(unittest.TestCase):
    @patch('requests.get')
    def test_call_count(self, mock_get):
        get_user_data()
        get_user_data()
        
        # モックが2回呼ばれたことを確認
        self.assertEqual(mock_get.call_count, 2)
        
        # 指定した引数で呼ばれたか確認
        mock_get.assert_called_with("https://api.example.com/user/1")

if __name__ == "__main__":
    unittest.main()

patch.object

特定のオブジェクトの属性やメソッドをモックに置き換えるための方法です。クラスのメソッドやインスタンス変数などに対してもモックを適用できます。

from unittest.mock import patch
import unittest

class User:
    def __init__(self):
        self.name = "Alice"

    def get_name(self):
        return self.name

class TestUser(unittest.TestCase):
    @patch.object(User, 'get_name', return_value="Mocked Name")
    def test_user_name(self, mock_get_name):
        user = User()
        self.assertEqual(user.get_name(), "Mocked Name")

if __name__ == "__main__":
    unittest.main()

MagicMockについて

MagicMockは、Mockのサブクラスで、特殊メソッド(マジックメソッド)を自動的にサポートしています。例えば、getitemlen、__iter__などの特殊メソッドを含むオブジェクトのモックに便利です。

Mockとの使い分けの基準

通常の関数やメソッドのモックが必要な場合 → Mockを使う

特殊メソッドやイテレーションが必要な場合 → MagicMockを使う

MagicMockの使用例

特殊メソッドのモック

from unittest.mock import MagicMock

# MagicMockオブジェクトを作成
magic_mock_obj = MagicMock()

# 辞書のような挙動
magic_mock_obj.__getitem__.return_value = 'value'

# 呼び出し
result = magic_mock_obj['key']
print(result)  # 'value'

# __len__などの特殊メソッドも使用可能
print(len(magic_mock_obj))  # デフォルトで1を返す
イテレーションのモック
from unittest.mock import MagicMock

# イテレーションをモック
magic_mock_obj = MagicMock()
magic_mock_obj.__iter__.return_value = iter([1, 2, 3])

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

結論

Mock: 特殊メソッドのサポートがなく、通常のメソッドや属性のモックに適している。基本的なモックオブジェクトを作る際に使う。

MagicMock: Mockの拡張版で、getitemlen、__iter__などの特殊メソッドが自動的にサポートされている。特殊メソッドやイテレーションを含むオブジェクトのモックに便利。

いいなと思ったら応援しよう!