0Pricing
AI Agents · レッスン

テストにおけるLLM呼び出しのモック

unittest.mock、pytestフィクスチャ、LLMレスポンスの記録と再生を学びます。

「テストにおけるLLM呼び出しのモック」はCoddyKit上の無料AI Agentsレッスンです。 これはレッスン2/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはAI Agents学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 AI Agentsコースには全4レッスンが含まれています。

モックとは

モックとは、実際の関数やオブジェクトを、制御されたレスポンスを返す偽物のバージョンに置き換えることです。エージェントのテストでは、LLM APIの呼び出しをモックすることで、テストを即座に実行でき、費用をかけずに、予測可能な結果を得られます。

Pythonでは、unittest.mockモジュールが標準的なツールです。

unittest.mock.patch()の基本

unittest.mock.patch(target)は、テストの実行中だけ、指定されたオブジェクトを一時的に置き換えます。targetには、テスト対象のモジュールでオブジェクトがどのようにインポートされているかを示す、ドット区切りの文字列を指定します。

from unittest.mock import patch, MagicMock

# The function under test calls openai.chat.completions.create
# We patch it so no real API call is made

def ask_llm(question: str) -> str:
    import openai
    client = openai.OpenAI(api_key='test')
    resp = client.chat.completions.create(
        model='gpt-4o-mini',
        messages=[{'role': 'user', 'content': question}]
    )
    return resp.choices[0].message.content

with patch('openai.OpenAI') as mock_client_class:
    mock_instance = MagicMock()
    mock_client_class.return_value = mock_instance
    mock_instance.chat.completions.create.return_value = MagicMock(
        choices=[MagicMock(message=MagicMock(content='Paris'))]
    )
    result = ask_llm('Capital of France?')
    print(result)  # 'Paris' — no API call made

pytestでデコレーターとしてpatchを使う

pytestでデコレーターとして使用すると、@patch()はモックを関数のパラメーターとして注入します。テストの完了後、モックは自動的に削除されます。

from unittest.mock import patch, MagicMock
import pytest

# Assume agent.py contains: import openai; client = openai.OpenAI(...)

@patch('agent.openai.OpenAI')
def test_agent_calls_llm(mock_openai_class):
    # Set up the mock chain
    mock_client = MagicMock()
    mock_openai_class.return_value = mock_client
    mock_client.chat.completions.create.return_value = MagicMock(
        choices=[MagicMock(message=MagicMock(content='Paris is the capital of France.'))]
    )

    from agent import ask_llm
    result = ask_llm('What is the capital of France?')

    assert 'Paris' in result
    mock_client.chat.completions.create.assert_called_once()

再利用可能なモックレスポンスの構築

モックのレスポンスオブジェクトを手作業で構築すると、コードが冗長になります。OpenAI SDKのレスポンス形式に一致する、正しい構造のモックオブジェクトを構築するヘルパー関数を作成します。

from unittest.mock import MagicMock

def make_mock_response(content: str, tool_calls: list = None) -> MagicMock:
    message = MagicMock()
    message.content = content
    message.tool_calls = tool_calls or []

    choice = MagicMock()
    choice.message = message
    choice.finish_reason = 'stop' if not tool_calls else 'tool_calls'

    response = MagicMock()
    response.choices = [choice]
    response.usage = MagicMock(total_tokens=42)
    return response

# Usage in tests:
# mock_create.return_value = make_mock_response('Hello!')
# mock_create.return_value = make_mock_response('', tool_calls=[...])

# --- demo ---
response = make_mock_response('The weather in Paris is 18C and sunny.')
print('content:', response.choices[0].message.content)
print('finish_reason:', response.choices[0].finish_reason)
print('total_tokens:', response.usage.total_tokens)

レスポンス内のツール呼び出しをモックする

エージェントのツール呼び出しロジックをテストする場合、モックのレスポンスには、エージェントの解析コードが正しく処理できるよう、適切な構造のtool_callsフィールドを含める必要があります。

import json
from unittest.mock import MagicMock

def make_tool_call_response(tool_name: str, arguments: dict) -> MagicMock:
    tool_call = MagicMock()
    tool_call.id = 'call_abc123'
    tool_call.type = 'function'
    tool_call.function = MagicMock()
    tool_call.function.name = tool_name
    tool_call.function.arguments = json.dumps(arguments)

    message = MagicMock()
    message.content = None
    message.tool_calls = [tool_call]

    response = MagicMock()
    response.choices = [MagicMock(message=message, finish_reason='tool_calls')]
    return response

# mock.return_value = make_tool_call_response('search_web', {'query': 'Python tutorials'})

# --- demo ---
response = make_tool_call_response('search_web', {'query': 'Python tutorials'})
call = response.choices[0].message.tool_calls[0]
print('tool name:', call.function.name)
print('tool arguments:', call.function.arguments)
print('finish_reason:', response.choices[0].finish_reason)

モック用のpytestフィクスチャ

pytestフィクスチャを使うと、再利用可能なセットアップコードを定義できます。LLMクライアントをパッチし、それを要求するテストに提供するフィクスチャを作成すれば、同じコードを繰り返し記述する必要がありません。

import pytest
from unittest.mock import patch, MagicMock

@pytest.fixture
def mock_openai(make_mock_response):
    with patch('myagent.client.chat.completions.create') as mock_create:
        mock_create.return_value = MagicMock(
            choices=[MagicMock(message=MagicMock(
                content='Default mocked response',
                tool_calls=[]
            ))]
        )
        yield mock_create

# Now any test can use it:
def test_agent_responds(mock_openai):
    from myagent import agent
    result = agent.run('Hello')
    assert result is not None
    mock_openai.assert_called_once()

pytest-mockのmockerフィクスチャ

pytest-mockは、パッチ処理を簡単にするmockerフィクスチャを提供します。モックを自動的にクリーンアップでき、通常のunittest.mock.patchよりもすっきりした構文で記述できます。

# pip install pytest-mock

# In your test file:
def test_agent_with_mocker(mocker):
    mock_create = mocker.patch('myagent.client.chat.completions.create')
    mock_create.return_value = mocker.MagicMock(
        choices=[mocker.MagicMock(message=mocker.MagicMock(
            content='Mocked answer',
            tool_calls=[]
        ))]
    )

    from myagent import agent
    result = agent.run('What is 2+2?')
    assert 'answer' in result.lower() or '4' in result

    mock_create.assert_called_once()
    # No cleanup needed — mocker handles it

vcr.pyで記録と再生を行う

vcrpyは、初回の実行時に実際のHTTP通信を「カセット」ファイルに記録し、その後の実行では記録を再生します。SDKではなく、生のHTTP APIを使用するコードのテストに最適です。

# pip install vcrpy
import vcr
import httpx

@vcr.use_cassette('fixtures/cassettes/openai_chat.yaml')
def test_with_recorded_response():
    # First run: makes a real HTTP call and records it
    # Subsequent runs: uses the recorded cassette (no network, no cost)
    response = httpx.post(
        'https://api.openai.com/v1/chat/completions',
        json={'model': 'gpt-4o-mini', 'messages': [{'role': 'user', 'content': 'Hello'}]},
        headers={'Authorization': 'Bearer YOUR_KEY'}
    )
    data = response.json()
    assert data['choices'][0]['message']['content'] is not None

モックが正しく呼び出されたことをアサートする

テスト後に、モックが正しい引数で呼び出されたことを確認します。これにより、エージェントが間違ったモデルを指定したり、パラメーターを省略したり、誤ったメッセージを送信したりするバグを検出できます。

from unittest.mock import patch, MagicMock, call

@patch('myagent.client.chat.completions.create')
def test_agent_sends_correct_model(mock_create):
    mock_create.return_value = MagicMock(
        choices=[MagicMock(message=MagicMock(content='ok', tool_calls=[]))]
    )

    from myagent import agent
    agent.run('Hello')

    # Verify the mock was called with correct arguments
    mock_create.assert_called_once()
    call_kwargs = mock_create.call_args.kwargs
    assert call_kwargs['model'] == 'gpt-4o-mini'
    assert len(call_kwargs['messages']) >= 1
    assert call_kwargs['messages'][0]['role'] == 'system'

テストでAPIエラーをシミュレートする

モックが例外を発生させるように設定して、エージェントがLLMの障害をどのように処理するかをテストします。実際のAPI障害を発生させることなく、エラー処理とリトライロジックを検証できます。

from unittest.mock import patch
import openai

@patch('myagent.client.chat.completions.create')
def test_agent_handles_rate_limit(mock_create):
    # Simulate a rate limit error
    mock_create.side_effect = openai.RateLimitError(
        message='Rate limit exceeded',
        response=None,
        body=None
    )

    from myagent import agent
    result = agent.run('Hello')

    # Agent should handle this gracefully
    assert result['error'] == 'rate_limit'
    # or
    assert result['retry_after'] is not None

conftest.pyでモックフィクスチャを整理する

共有フィクスチャは、テストディレクトリのルートにあるconftest.pyに配置します。pytestはこのファイルを自動的に検出し、インポートしなくても、すべてのテストファイルでフィクスチャを利用できるようにします。

# tests/conftest.py
import pytest
from unittest.mock import patch, MagicMock

@pytest.fixture(autouse=False)
def mock_llm():
    with patch('myagent.client.chat.completions.create') as mock_create:
        mock_create.return_value = MagicMock(
            choices=[MagicMock(message=MagicMock(
                content='Test response',
                tool_calls=[]
            ))]
        )
        yield mock_create

@pytest.fixture
def mock_search_tool():
    with patch('myagent.tools.search_web') as mock_search:
        mock_search.return_value = [{'title': 'Test', 'url': 'https://example.com'}]
        yield mock_search

理解度チェック:LLM呼び出しのモック

エージェントのテストで使うモック技法について、理解度を確認しましょう。

復習:テストでのLLM呼び出しのモック

これで、高速で信頼性の高いエージェントの単体テストを書くためのツールがそろいました。

  • unittest.mock.patch()を使って、LLMクライアントをモックに置き換えます
  • SDKのレスポンス形式に一致する、再利用可能なモックレスポンスヘルパーを構築します
  • 適切な構造のtool_callsフィールドを使って、ツール呼び出しをモックします
  • pytestフィクスチャとconftest.pyを使って、テスト間でモックを共有します
  • pytest-mockを使って、より簡潔な構文で記述します
  • vcrpyを使って、実際のHTTP通信を記録・再生します

モックは、高速で保守しやすいエージェントのテストスイートの基盤です。

よくある質問

「テストにおけるLLM呼び出しのモック」レッスンは無料ですか?

はい。「テストにおけるLLM呼び出しのモック」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、AI Agentsコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 AI Agentsコースには全4レッスンが含まれています。

「テストにおけるLLM呼び出しのモック」で何を学びますか?

unittest.mock、pytestフィクスチャ、LLMレスポンスの記録と再生を学びます。 ブラウザで直接実行するハンズオンコードでAI Agentsを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。

AI Agentsを始めるのに経験は必要ですか?

事前経験は必要ありません。CoddyKitのAI Agentsは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン2/4です。

「テストにおけるLLM呼び出しのモック」レッスンにはどのくらい時間がかかりますか?

ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。

このAI Agentsレッスンでコードを書いて実行できますか?

はい。すべてのAI Agentsレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。

このコースのすべてのレッスン

  1. エージェントのテストが異なる理由
  2. テストにおけるLLM呼び出しのモック
  3. アサーションベースのエージェントテスト
  4. エージェントパイプラインの統合テスト
← AI Agentsに戻る