Имитация вызовов LLM в тестах
unittest.mock, фикстуры pytest и запись с последующим воспроизведением ответов LLM.
«Имитация вызовов LLM в тестах» — бесплатный урок AI Agents на CoddyKit. Это урок 2 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения AI Agents, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс AI Agents содержит 4 уроков всего.
Что такое имитация
Имитация заменяет реальную функцию или объект фиктивной версией, которая возвращает контролируемые ответы. При тестировании агентов мы имитируем вызовы API LLM, чтобы тесты выполнялись мгновенно, не требовали затрат и давали предсказуемые результаты.
Стандартным инструментом для этого служит модуль 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Использование patch как декоратора в pytest
При использовании в качестве декоратора с 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 позволяют определить повторно используемый код настройки. Создайте fixture, которая заменяет клиент 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()Фикстура mocker из pytest-mock
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 в файл «кассету», а при последующих запусках воспроизводит их. Это особенно удобно для тестирования кода, использующего низкоуровневый HTTP API, а не SDK.
# 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/7) и разблокировать остальной курс AI Agents, подпишись на CoddyKit PRO. Курс AI Agents содержит 4 уроков всего.
Чему я научусь в уроке «Имитация вызовов LLM в тестах»?
unittest.mock, фикстуры pytest и запись с последующим воспроизведением ответов LLM. Ты практикуешь AI Agents с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.
Нужен ли мне опыт, чтобы начать AI Agents?
Предыдущий опыт не требуется. AI Agents на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 2 из 4.
Сколько времени занимает урок «Имитация вызовов LLM в тестах»?
Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.
Можно ли писать и запускать код в этом уроке AI Agents?
Да. Каждый урок AI Agents включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.
Все уроки этого курса
- Почему тестирование агентов отличается
- Имитация вызовов LLM в тестах
- Тестирование агентов на основе утверждений
- Интеграционные тесты для конвейеров агентов