0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Pythonのテストが一気に楽になる!MagicMock基礎と実務で使える3つのパターン

0
Posted at

Pythonでテストを書いていると、次のような悩みに直面することがあります。

  • 外部APIを呼び出す処理があり、テストのたびに通信するのは避けたい
  • DB接続の準備が必要になり、テストが重くなる
  • テスト対象の関数の中で別クラスや別関数が呼ばれていて、思った通りにテストできない

「この処理のロジックだけ確かめたいのに、なぜ余計な準備が必要なのだろう」
そう感じる瞬間は多いです。テストは負担を増やすものではなく、安心して開発するための仕組みであるべきです。

そこで役立つのが MagicMock です。本記事では、MagicMockとは何か、どのように使うとテストが楽になるのかを、実務レベルのコード例を交えてわかりやすく解説します。


1. MagicMockとは何か?

MagicMock は Python 標準ライブラリ unittest.mock に含まれる、偽のオブジェクト(モック)を作成する仕組みです。
偽物とはいえ、次のような特徴があります。

  • 既存のクラスや関数の代わりに使用できる
  • 戻り値を自由に設定できる
  • 呼び出し回数や引数を検証できる

つまり、外部に依存したくないテストを、純粋にロジックだけ確認できる状態に変えてくれる道具です。

テストを書くうえでの本音はこうです。

「DBやAPIの動作ではなく、テスト対象の関数が正しい結果を返しているかだけ知りたい」

MagicMockはその気持ちを叶えてくれます。


2. MagicMockの最小例(依存クラス置き換えパターン)

まずは、MagicMockの基本的な使い方を見ていきます。

実装コード

# case1/main.py

class ApiClient:
    def __init__(self, user_id: str):
        self.user_id = user_id

    def get_user(self, user_id):
        res = self.user_id if self.user_id == user_id else "error"
        return res


def fetch_user(api_client: ApiClient, user_id: str) -> str:
    return "user_id:" + str(api_client.get_user(user_id))

fetch_user() をテストしたいだけなのに、ApiClient の実装まで準備したくない場合があります。
本来確認したいのは fetch_user の戻り値だけであり、ApiClient の動作そのものではありません。


MagicMockで依存を無効化する

# case1/test_fetch_user.py
from unittest.mock import MagicMock
from case1.main import fetch_user

def test_fetch_user():
    mock_client = MagicMock()
    mock_client.get_user.return_value = "magic_mock_return_value"

    result = fetch_user(mock_client, "magic_mock_return_value")

    mock_client.get_user.assert_called_once_with("magic_mock_return_value")
    assert result == "user_id:magic_mock_return_value"

このテストが成立する理由

観点 MagicMockの役割
本物のクラス 不要
呼び出すメソッド MagicMockが代替
戻り値 return_valueで指定可能
呼び出し検証 assert_called_once_withで確認

MagicMockを渡すだけで、依存クラスが存在するように“見せかける”環境が完成します。


3. しかし、MagicMockだけでは解決できないケースがある

ここまで見ると、「MagicMockさえあれば全部解決できるのでは?」と思うかもしれません。
しかし、実務ではそう簡単ではないことがあります。

次のコードを見てください。

# case2/email.py
def send_email(to: str, body: str) -> bool:
    """本物は外部のメールサーバに送る、というイメージ"""
    print(f"Send mail to {to}: {body}")
    # 実際には失敗することもあるが、ここではTrueで決め打ち
    return True
# case2/notify.py
from case2.email import send_email

def notify_user(user_id: int, message: str) -> bool:
    email = f"user{user_id}@example.com"
    return send_email(email, message)

この関数には問題があります。

  • send_email を引数で受け取っていない
    → MagicMockを外から渡す余地がない

つまり、MagicMock単体では置き換えられません。

ここで必要になるのが patch です。


4. なぜ patch が必要なのか?

MagicMock と patch の役割の違い

名前 何をする? 正体
MagicMock 偽物の関数やクラスを作る モック本体
patch その偽物を指定した場所に差し込む 差し替え装置

MagicMockは「部品」
patchは「部品交換に使う工具」

というイメージです。


5. patchを使ったテスト例

from unittest.mock import patch
from case2.notify import notify_user

def test_notify_user_with_patch():
    with patch("case2.notify.send_email") as mock_send_email:
        mock_send_email.return_value = True

        result = notify_user(1, "Test Message")

        mock_send_email.assert_called_once_with("user1@example.com", "Test Message")
        assert result is True

ここで起きていること

  • case2.notify.send_email の呼び出し先が MagicMock に置き換わる
  • 本物のメール送信は実行されない
  • 呼び出し履歴を検証できる

MagicMock単体では不可能な状況を patch が解決している

"case2.email.send_email"ではないのか?と思った方は鋭いです。
patchを使う場合、テストしたい関数に視点をおくことが重要になります。

今回テストしたい関数は case2/notify.pynotify_userであり、その中で呼び出しているsend_email関数をダミーに置換したいのでした。
ここでの視点はあくまでもcase2/notify.pyです。
したがって、case2/notify.pyから見ると、send_emailcase2.notify.send_emailという名前空間にあるため、patch()は、"case2.notify.send_email"が正しいのです。


6. 外部APIやLLM呼び出しにも応用できる

外部サービスと通信する関数もテストしやすくなります。

# case3/call_llm.py
import os

from litellm import completion

api_key = os.environ.get("GEMINI_API_KEY")


def call_gemini():
    response = completion(
        model="gemini/gemini-2.5-flash",
        api_key=api_key,
        messages=[{"role": "user", "content": "こんにちは!Geminiで何ができますか?"}],
    )

    content = response.choices[0].message.content
    return content


def main():
    content = call_gemini()
    print(f"{content=}")


if __name__ == "__main__":
    main()

from unittest.mock import patch
from case3.call_llm import main

def test_case3_main_with_patch():
    with patch("case3.call_llm.call_gemini") as mock_call_gemini:
        mock_call_gemini.return_value = "モックされた応答"
        main()
        mock_call_gemini.assert_called_once()

APIの速度やコストに左右されなくなり、テストの安定性が向上します。


7. MagicMockを使うべき場面

状況 MagicMockで解決可能? 理由
外部サービスやDBに依存 テストが遅く不安定になる
返り値だけ変えたい MagicMock単体で対応できる
呼び出し履歴を確認したい assert系が活躍
関数内部の依存関数を差し替えたい patchが必要になる

8. まとめ

MagicMockは、テストを書くときの次の悩みを解消してくれます。

  • 外部依存を切り離してロジックだけ確認できる
  • テストが速く・安定する
  • 呼び出し履歴まで検証できる
  • 余計なセットアップが不要になる

そして、MagicMockだけでは置き換えられないケースに直面したとき
その隙間を埋めてくれるのが patch です。

MagicMockとpatchを理解して使い分けられるようになると、テストは一気に楽になります。
テストが負担ではなく、安心して開発を進めるための武器になるはずです。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?