mcpField
P
pydantic-ai-testing

pydantic-ai-testing

github.com/existential-birdsテスト・品質85146 回インストール11 週間前に更新
リポジトリ

pydantic-ai-testing について

PydanticAIエージェントのテストを書く開発者向けのスキルです。決定的なテストや記録済みのモデル対話の検証に使う、モデルの代替、依存関係の上書き、スナップショット、カセット更新手順を扱います。

主な機能

  1. 01TestModelによる決定的なテスト
  2. 02FunctionModelによる応答のカスタマイズ
  3. 03VCRによるAPI対話の記録
  4. 04inline_snapshotによる出力検証
  5. 05ツールと依存関係のテストパターン
  6. 06対象を限定したカセットとスナップショットの更新

活用シーン

  1. 01APIを呼び出さないエージェントのテスト
  2. 02モデル応答とツール呼び出しの制御
  3. 03記録済みLLM対話の再生
  4. 04出力構造と取得メッセージの検証

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/existential-birds/beagle --skill pydantic-ai-testing

インストール後、Claude Code で/pydantic-ai-testingを入力して読み込まれたか確認します。

詳細

PydanticAI エージェントのテスト

TestModel(決定論的なテスト)

API を呼び出さずにテストするには、TestModel を使用します。

import pytest
from pydantic_ai import Agent
from pydantic_ai.models.test import TestModel

def test_agent_basic():
    agent = Agent('openai:gpt-4o')

    # Override with TestModel for testing
    result = agent.run_sync('Hello', model=TestModel())

    # TestModel generates deterministic output based on output_type
    assert isinstance(result.output, str)

TestModel の設定

from pydantic_ai.models.test import TestModel

# Custom text output
model = TestModel(custom_output_text='Custom response')
result = agent.run_sync('Hello', model=model)
assert result.output == 'Custom response'

# Custom structured output (for output_type agents)
from pydantic import BaseModel

class Response(BaseModel):
    message: str
    score: int

agent = Agent('openai:gpt-4o', output_type=Response)
model = TestModel(custom_output_args={'message': 'Test', 'score': 42})
result = agent.run_sync('Hello', model=model)
assert result.output.message == 'Test'

# Seed for reproducible random output
model = TestModel(seed=42)

# Force tool calls
model = TestModel(call_tools=['my_tool', 'another_tool'])

オーバーライド用コンテキストマネージャー

from pydantic_ai import Agent
from pydantic_ai.models.test import TestModel

agent = Agent('openai:gpt-4o', deps_type=MyDeps)

def test_with_override():
    mock_deps = MyDeps(db=MockDB())

    with agent.override(model=TestModel(), deps=mock_deps):
        # All runs use TestModel and mock_deps
        result = agent.run_sync('Hello')
        assert result.output

FunctionModel(カスタムロジック)

モデルの応答を完全に制御するには、次のようにします。

from pydantic_ai import Agent, ModelMessage, ModelResponse, TextPart
from pydantic_ai.models.function import AgentInfo, FunctionModel

def custom_model(
    messages: list[ModelMessage],
    info: AgentInfo
) -> ModelResponse:
    """Custom model that inspects messages and returns response."""
    # Access the last user message
    last_msg = messages[-1]

    # Return custom response
    return ModelResponse(parts=[TextPart('Custom response')])

agent = Agent(FunctionModel(custom_model))
result = agent.run_sync('Hello')
ツール呼び出しを伴う FunctionModel
from pydantic_ai import ToolCallPart, ModelResponse
from pydantic_ai.models.function import AgentInfo, FunctionModel

def model_with_tools(
    messages: list[ModelMessage],
    info: AgentInfo
) -> ModelResponse:
    # First request: call a tool
    if len(messages) == 1:
        return ModelResponse(parts=[
            ToolCallPart(
                tool_name='get_data',
                args='{"id": 123}'
            )
        ])

    # After tool response: return final result
    return ModelResponse(parts=[TextPart('Done with tool result')])

agent = Agent(FunctionModel(model_with_tools))

@agent.tool_plain
def get_data(id: int) -> str:
    return f"Data for {id}"

result = agent.run_sync('Get data')

VCR カセット(記録された API 呼び出し)

実際の LLM API とのやり取りを記録して再生します。

import pytest

@pytest.mark.vcr
def test_with_recorded_response():
    """Uses recorded cassette from tests/cassettes/"""
    agent = Agent('openai:gpt-4o')
    result = agent.run_sync('Hello')
    assert 'hello' in result.output.lower()

# To record/update cassettes:
# uv run pytest --record-mode=rewrite tests/test_file.py

カセットファイルは YAML 形式で tests/cassettes/ に保存されます。

インラインスナップショット

自動更新されるスナップショットを使って、期待される出力を検証します。

from inline_snapshot import snapshot

def test_agent_output():
    result = agent.run_sync('Hello', model=TestModel())

    # First run: creates snapshot
    # Subsequent runs: asserts against it
    assert result.output == snapshot('expected output here')

# Update snapshots:
# uv run pytest --inline-snapshot=fix

ゲート:VCR カセットとインラインスナップショット

記録または修正を行うと、ディスク上のファイルが書き換えられます。この手順に従い、ステップを省略しないでください。

  1. 再生パス(記録/修正フラグなし): 対象のパスに対して uv run pytest を実行します。すべて成功すること(または、失敗の原因が把握されており、更新対象の成果物とは無関係であること)。
  2. 対象範囲を確定: 更新する tests/cassettes/ 内のカセット、または snapshot(...) アサーションを特定し、変更されるファイルがそれらだけであることを確認します。
  3. 記録または修正: 対象のパスに対して、範囲を限定したコマンドを1つだけ実行します。uv run pytest --record-mode=rewrite … または uv run pytest --inline-snapshot=fix … を実行します。
  4. 事後条件: 記録/修正フラグを付けずに同じテストをもう一度実行し、すべて成功することを確認します。git diff を確認し、予期された .yaml/スナップショットの変更だけであることを確かめます。

ステップ 4 が失敗した場合は、意図しない差分を元に戻し、再記録する前にテストまたはモデルを修正してください。

ツールのテスト

from pydantic_ai import Agent, RunContext
from pydantic_ai.models.test import TestModel

def test_tool_is_called():
    agent = Agent('openai:gpt-4o')
    tool_called = False

    @agent.tool_plain
    def my_tool(x: int) -> str:
        nonlocal tool_called
        tool_called = True
        return f"Result: {x}"

    # Force TestModel to call the tool
    result = agent.run_sync(
        'Use my_tool',
        model=TestModel(call_tools=['my_tool'])
    )

    assert tool_called

依存関係を使用したテスト

from dataclasses import dataclass
from unittest.mock import AsyncMock

@dataclass
class Deps:
    api: ApiClient

def test_tool_with_deps():
    # Create mock dependency
    mock_api = AsyncMock()
    mock_api.fetch.return_value = {'data': 'test'}

    agent = Agent('openai:gpt-4o', deps_type=Deps)

    @agent.tool
    async def fetch_data(ctx: RunContext[Deps]) -> dict:
        return await ctx.deps.api.fetch()

    with agent.override(
        model=TestModel(call_tools=['fetch_data']),
        deps=Deps(api=mock_api)
    ):
        result = agent.run_sync('Fetch data')

    mock_api.fetch.assert_called_once()

メッセージの取得

実行時のすべてのメッセージを確認します。

from pydantic_ai import Agent, capture_run_messages

agent = Agent('openai:gpt-4o')

with capture_run_messages() as messages:
    result = agent.run_sync('Hello', model=TestModel())

# Inspect captured messages
for msg in messages:
    print(msg)

テストパターンのまとめ

シナリオ方法
API を使わないユニットテストTestModel()
カスタムモデルロジックFunctionModel(func)
記録された実際の応答@pytest.mark.vcr
出力構造の検証inline_snapshot
ツールが呼び出されることのテストTestModel(call_tools=[...])
依存関係のモックagent.override(deps=...)

pytest の設定

一般的な pyproject.toml:

[tool.pytest.ini_options]
testpaths = ["tests"]
asyncio_mode = "auto"  # For async tests

テストを実行します。

uv run pytest tests/test_agent.py -v
uv run pytest --inline-snapshot=fix  # Update snapshots

コメント