Реализовать simulation testing для AI-агента
ТЕХНИЧЕСКОЕ ЗАДАНИЕ: Реализовать simulation testing для AI-агента
1. Цель задачи
Научиться проектировать и реализовывать simulation testing окружение для AI-агента, которое позволяет имитировать работу внешних сервисов и целенаправленно внедрять сбои (injection|fault injection). Результатом станет набор автоматизированных тестов, покрывающих редкие сценарии (отказы API, таймауты, невалидные ответы, задержки), которые сложно воспроизвести в реальном окружении.
Ключевой результат Написаны и выполняются тесты, эмулирующие не менее 5 типов редких сбоев, с проверкой корректной обработки агентом (retry, fallback, логирование, корректный ответ пользователю).
2. Исходные данные
| Что нужно | Откуда взять |
|---|---|
| AI-агент (простой, с вызовом внешнего REST API) | Пет-проект (например, агент для получения погоды/перевода) или создать в рамках задачи |
| Спецификация внешнего API (эндпоинты, форматы запросов/ответов) | Документация сервиса (OpenAPI) или описание, заданное вручную |
| Инструмент mock API | WireMock (Docker-образ), или in-process mock (responses / aioresponses) |
| Фреймворк для тестирования | pytest + asyncio (если агент асинхронный) |
| Инструмент fault injection | Самописный middleware / декоратор (или Chaos Toolkit для HTTP) |
| Базовый CI (опционально) | GitHub Actions / GitLab CI |
Если нет реального инструмента — симулируем:
- Создать простого AI-агента на Python (например, на базе LangChain или просто класс с методом
get_weather(city), который делает HTTP GET к api.weather.example). - Развернуть WireMock в Docker (docker run -p 8080:8080 wiremock/wiremock) или написать mock-сервер на FastAPI (один файл).
- Написать декоратор
inject_faultдля подмены запросов в тестах (с помощью responses для requests или aioresponses для aiohttp).
3. Технологический стек
| Компонент | Инструменты | Назначение |
|---|---|---|
| AI-агент | Python 3.11+, aiohttp / requests, LangChain (опционально) | Целевая система, которую тестируем |
| Mock API | WireMock (Docker) / responses / aioresponses | Замена реальных внешних сервисов на контролируемые заглушки |
| Fault injection | Самописный middleware / responses callback / Chaos Toolkit | Внедрение сбоев (timeout, 500, 429, битый JSON) |
| Тестовый фреймворк | pytest, pytest-asyncio, pytest-timeout | Запуск тестов и проверка утверждений |
| Логирование | structlog / loguru | Запись всех шагов агента для отладки тестов |
| Метрики (опционально) | prometheus_client | Счётчики ошибок по типу сбоя |
| CI (опционально) | GitHub Actions | Автоматический прогон тестов на каждый PR |
4. Этапы выполнения
Этап 1: Подготовка тестового агента (оценка 30-60 мин)
Действия
- Если нет готового агента — написать простого агента, который вызывает одно внешнее REST API (например,
GET /v1/weather?city=...). - Реализовать обработку ответа: извлечение температуры, возврат строки пользователю.
- Добавить базовую логику повторных попыток (retry) при 5xx и таймаутах (максимум 3 попытки).
- Написать конфиг с адресом внешнего сервиса (через переменные окружения).
- Убедиться, что агент работает с реальным mock-сервером без сбоев.
# agent.py (упрощённый пример)
import asyncio
import aiohttp
class WeatherAgent:
def __init__(self, api_url: str):
self.api_url = api_url
self.max_retries = 3
async def get_weather(self, city: str) -> str:
for attempt in range(self.max_retries):
try:
async with aiohttp.ClientSession() as session:
async with session.get(f"{self.api_url}/v1/weather", params={"city": city}) as resp:
if resp.status == 200:
data = await resp.json()
return f"Temperature: {data['temp']}°C"
elif resp.status >= 500:
raise aiohttp.HttpProcessingError(f"Server error {resp.status}")
except (aiohttp.ClientError, asyncio.TimeoutError) as e:
if attempt == self.max_retries - 1:
raise
await asyncio.sleep(2 ** attempt)
Ожидаемый результат этапа Рабочий агент, который корректно обрабатывает успешные ответы mock-сервера.
Этап 2: Разработка mock API для внешних сервисов (1-2 часа)
Действия
-
Выбрать способ mock-сервера WireMock (рекомендуется для реалистичности) или in-process mock с aioresponses.
-
In-process mock (альтернатива):
- Использовать aioresponses как контекстный менеджер в тестах.
- Определить фикстуру, которая отключает реальные HTTP запросы и подменяет ответы.
-
Создать несколько профилей ответов
- Успешный (200, нормальные данные)
- Пустой ответ (200, битый JSON)
- Неверный статус (404, 429, 503)
- Задержка (delay 5 сек – имитация таймаута)
-
Добавить конфигурацию mock-сервера в тестовую фикстуру pytest.
# conftest.py (пример с aioresponses)
import pytest
from aioresponses import aioresponses
@pytest.fixture
def mock_weather():
with aioresponses() as m:
m.get("http://test-api/v1/weather", status=200, body='{"temp":22}')
yield m
Ожидаемый результат этапа Mock-сервер готов, тесты с успешным сценарием проходят.
Этап 3: Внедрение fault injection (1-1.5 часа)
Действия
-
Определить набор сбоев для тестирования
-
Реализовать механизм fault injection двумя способами:
- Статический в каждом тесте задать конкретный сбой.
- Динамический декоратор / фикстура, которая с вероятностью X подменяет ответ.
-
Сценарий статического fault injection через aioresponses:
@pytest.mark.parametrize("fault_type", ["timeout", "500", "429", "bad_json", "empty"])
async def test_fault_scenarios(agent, fault_type):
with aioresponses() as m:
if fault_type == "timeout":
m.get("http://test-api/v1/weather", exception=asyncio.TimeoutError)
elif fault_type == "500":
m.get("http://test-api/v1/weather", status=500)
elif fault_type == "429":
m.get("http://test-api/v1/weather", status=429, headers={"Retry-After": "2"})
elif fault_type == "bad_json":
m.get("http://test-api/v1/weather", status=200, body='not json')
elif fault_type == "empty":
m.get("http://test-api/v1/weather", status=200, body='null')
result = await agent.get_weather("Moscow")
# Проверить, что агент возвращает fallback-сообщение, а не крашится
assert "ошибка" in result.lower() or "недоступен" in result.lower()
- Добавить логирование в агент (например, через structlog), чтобы в логах теста было видно, какие попытки были.
Ожидаемый результат этапа Набор фикстур и параметризованных тестов, покрывающих 5 типов сбоев. Агент не падает, а возвращает понятное сообщение.
Этап 4: Написание simulation тестов и проверка устойчивости (1-2 часа)
Действия
-
Дополнить тесты проверкой:
- Количество попыток retry (не более 3)
- Время выполнения (при таймауте не превышает заданный таймаут)
- Логирование каждого сбоя (через захват логов в тесте)
- Метрики (если внедрили счётчики)
-
Написать тест на восстановление после временной ошибки (агент должен корректно обработать сервис, который сначала падает, а потом отвечает успешно).
async def test_recovery_after_fault(agent):
with aioresponses() as m:
# Первые два запроса – 500, третий – успех
m.get("http://test-api/v1/weather", status=500, repeat=2)
m.get("http://test-api/v1/weather", status=200, body='{"temp":25}', repeat=1)
result = await agent.get_weather("Moscow")
assert "25" in result
# Убедиться, что было ровно 3 попытки
-
Добавить тесты на конкурирующие сценарии (если агент асинхронный): одновременные запросы с разными сбоями.
-
Оформить отчёт о покрытии редких сценариев (Markdown список).
Ожидаемый результат этапа Все тесты проходят, фиксируются логи, метрики (если есть). Отчёт о покрытии.
Этап 5: Интеграция в CI (оценка 45 мин)
Действия
- Создать Makefile с командой
make test. - Добавить GitHub Actions workflow
.github/workflows/test.yml:- Установка Python и зависимостей
- Запуск
pytest --timeout=60 -v --tb=short
- Настроить вывод отчёта (pytest-html или junit.xml).
- Убедиться, что mock-сервер не требуется извне – все тесты используют in-process mock.
Ожидаемый результат этапа Тесты автоматически запускаются на каждый PR, результат доступен.
5. Критерии приемки (Definition of Done)
- Реализован mock API для всех внешних вызовов агента (минимум 1 эндпоинт)
- Fault injection покрывает не менее 5 типов ошибок (таймаут, 500, 429, битый JSON, пустой ответ)
- Написаны автоматизированные тесты для каждого типа ошибки (параметризованные)
- Тесты проверяют, что агент не падает, а возвращает ожидаемое fallback-сообщение
- Добавлена фикстура для захвата логов, проверяющая, что ошибки логируются
- Реализован тест на восстановление после временной ошибки (2 сбоя → успех)
- Тесты запускаются одной командой (
pytest) и проходят в CI - Все тесты не требуют доступа к внешним сервисам
- В README описано, как запустить тесты и добавить новый сценарий
6. Ожидаемый результат
Основные артефакты
- Репозиторий с кодом агента (
agent.py) - Тесты (
test_*.py) и фикстуры (conftest.py) - Конфигурация CI (
.github/workflows/test.yml) - Файл
requirements.txtилиpyproject.tomlс зависимостями README.mdс описанием структуры, запуска и примером добавления нового сбоя- Отчёт о покрытии редких сценариев (таблица в Markdown)
Опционально дашборд метрик (если внедрён prometheus_client), логи тестов в stdout в формате JSON.
7. Возможные сложности и их решение
| Сложность | Решение |
|---|---|
| WireMock не стартует или порт занят | Использовать in-process mock (aioresponses / responses) — они не требуют внешнего процесса |
| Таймауты в CI из-за ждущих тестов | Установить pytest-timeout и задать общий таймаут на тест (например, 30 сек) |
| Асинхронные вызовы сложно mock-aть | Использовать aioresponses или подменить транспорт на unittest.mock внутри агента |
| Агент использует библиотеку, которая не поддерживает перехват (например, httpx) | Установить pytest-httpx или использовать respx для httpx |
| Fault injection не всегда отрабатывает (вдруг настоящий API вызвался) | Всегда включать режим passthrough=False в mock-библиотеке; в conftest.py написать фикстуру, блокирующую любые реальные запросы |
8. Бюджет времени (оценка)
| Этап | Время |
|---|---|
| Этап 1: Подготовка тестового агента | 30–60 мин |
| Этап 2: Разработка mock API | 1–2 часа |
| Этап 3: Внедрение fault injection | 1–1.5 часа |
| Этап 4: Написание simulation тестов | 1–2 часа |
| Этап 5: Интеграция в CI | 45 мин |
| Итого | 4.25–7.25 часов |
Примечание Для первого исполнения заложите 8–10 часов с учётом исследования инструментов и отладки.
9. Связанные вопросы из базы знаний
| Вопрос | Тема |
|---|---|
| 14 | QA testing: стратегии тестирования AI-агентов |
| 23 | Mocking в Python: unittest.mock vs responses vs aioresponses |
| 45 | Fault injection techniques для микросервисов |
| 67 | Resilience patterns: retry, circuit breaker, fallback |
| 89 | Integration testing для асинхронных систем на asyncio |
| 112 | Pytest фикстуры и параметризация тестов |
| 145 | Контейнеризация mock-серверов (WireMock) |
| 201 | Metrics-driven testing: использование счётчиков в тестах |
| 267 | Chaos engineering для REST API на практике |
| 320 | CI/CD pipeline для Python: pytest + GitHub Actions |
10. Чек-лист самопроверки
- Я настроил mock-сервер, который возвращает все необходимые ответы (успех и сбои)
- Я внедрил fault injection для минимум 5 типов сбоев (таймаут, 500, 429, битый JSON, пустой ответ)
- Я написал параметризованные тесты, которые проверяют реакцию агента на каждый сбой (не падает, возвращает fallback)
- Я убедился, что тесты запускаются одной командой (
pytest) и проходят без доступа к внешним ресурсам - Я добавил логирование ошибок и (опционально) метрики ошибок по типу
- Я оформил README с описанием как запустить тесты и добавить новый сценарий