中文翻译暂不可用,显示俄语原文。
Реализовать tool testing изолированно
ТЕХНИЧЕСКОЕ ЗАДАНИЕ: Реализовать tool testing изолированно
1. Цель задачи
Научиться unit-тестировать каждый инструмент (tool) AI-агента в изоляции от LLM и внешних зависимостей. Разработать набор тестов, который покрывает все логические ветви каждого инструмента (100% branch coverage) и проверяет корректность работы при нормальных и граничных входных данных.
Ключевой результат Исполняемый pytest-набор с coverage >= 100% по строкам для каждого инструмента, запускаемый одной командой.
2. Исходные данные
| Что нужно | Откуда взять |
|---|---|
| Кодовая база агента с инструментами | Репозиторий проекта (например, pet-проект или существующий агент) |
| Список всех инструментов | Документация или модули tools/ |
| Тестовая среда Python | Локальная установка Python 3.10+ |
| Библиотека для mocking | unittest.mock или pytest-mock |
| Инструмент для измерения покрытия | pytest-cov |
Если нет готового проекта с инструментами — симулируем:
- Создать простую библиотеку из 3–4 инструментов (например,
get_weather,search_database,send_email,calculator). - Каждый инструмент должен иметь как минимум один вызов внешнего API или чтение файла/конфига.
- Написать фиктивные реализации внешних сервисов (stub) для тестирования без реального подключения.
3. Технологический стек
| Компонент | Инструменты | Назначение |
|---|---|---|
| Язык программирования | Python 3.10+ | Реализация инструментов и тестов |
| Тестовый фреймворк | pytest 7.x | Запуск и организация тестов |
| Mocking | pytest-mock / unittest.mock | Изоляция внешних зависимостей |
| Покрытие кода | pytest-cov | Измерение и отчет по coverage |
| Фикстуры | pytest fixtures | Подготовка тестовых данных |
| CI/CD (опционально) | GitHub Actions | Автоматический прогон тестов |
4. Этапы выполнения
Этап 1: Анализ инструментов и установка окружения (20–30 минут)
Действия
-
Определить список инструментов, подлежащих тестированию
- Пройтись по коду проекта, выписать все классы/функции, декорированные как @tool или используемые агентом.
- Для каждого инструмента записать:
- входные параметры (типы, значения по умолчанию)
- возможные исключения (ConnectionError, ValueError, FileNotFoundError и т.д.)
- внешние зависимости (API-вызовы, БД, файловая система)
- Результат занести в таблицу (например, в Google Sheets или Markdown).
-
Установить тестовые зависимости
pip install pytest pytest-cov pytest-mock -
Настроить структуру тестов
- Создать папку
tests/на одном уровне сtools/. - Создать файл conftest.py для общих фикстур.
- Создать папку
Ожидаемый результат этапа Список инструментов с зонами тестирования, готовое окружение.
Этап 2: Написание unit-тестов для первого инструмента (1–2 часа)
Действия
-
Выбрать простейший инструмент (например,
calculatorилиget_current_time). -
Написать тесты на нормальные сценарии:
- Все обязательные параметры переданы корректно.
- Типы данных совпадают с ожидаемыми.
# tests/test_calculator.py import pytest from tools.calculator import CalculatorTool class TestCalculatorTool: def test_add_positive(self): tool = CalculatorTool() result = tool.run("add", 3, 4) assert result == 7 def test_divide_by_zero_raises(self): tool = CalculatorTool() with pytest.raises(ValueError, match="division by zero"): tool.run("divide", 10, 0) -
Написать тесты на граничные случаи:
- Пустые строки,
None, отрицательные числа, переполнение.
- Пустые строки,
-
Написать тесты на обработку ошибок:
- Неверный тип аргумента, отсутствие обязательного параметра, внешний сбой.
-
Запустить тесты и проверить покрытие:
pytest tests/test_calculator.py --cov=tools.calculator --cov-report=term-missing- Добиться 100% покрытия строк и веток (если инструмент содержит ветвления).
Ожидаемый результат этапа Полный набор тестов для первого инструмента с 100% coverage.
Этап 3: Изоляция внешних зависимостей (1–1.5 часа)
Действия
-
Для инструментов, вызывающих внешние API (например,
get_weather), создать моки.# tests/test_weather.py import pytest from unittest.mock import Mock, patch from tools.weather import WeatherTool class TestWeatherTool: @patch("tools.weather.requests.get") def test_successful_fetch(self, mock_get): mock_response = Mock() mock_response.status_code = 200 mock_response.json.return_value = {"temp": 22, "unit": "C"} mock_get.return_value = mock_response tool = WeatherTool() result = tool.run("London") assert result == {"temp": 22, "unit": "C"} mock_get.assert_called_once_with("https://api.weather.com/v1/London") -
Для инструментов, работающих с файловой системой, использовать
tmp_pathфикстуру pytest илиpyfakefs. -
Для инструментов с вызовом LLM (например,
summarize), замокать LLM-клиент на уровне mocked_client.generate(). -
Проверить, что тесты проходят без доступа к реальным сервисам.
-
Добавить фикстуры в conftest.py:
@pytest.fixture def mock_llm_client(): with patch("tools.summarize.LLMClient") as mock: mock.return_value.generate.return_value = "mocked summary" yield mock
Ожидаемый результат этапа Все внешние зависимости заменены на моки; тесты выполняются изолированно.
Этап 4: Достижение 100% покрытия для всех инструментов (2–4 часа)
Действия
-
Для каждого оставшегося инструмента повторить этапы 2 и 3.
-
Особое внимание уделить:
- Исключительным путям (except-блоки).
- Ветвям if-elif-else.
- Циклам (пустой список, один элемент, много).
- Конфигурационным параметрам, влияющим на поведение.
-
Создать общий тестовый раннер:
pytest tests/ --cov=tools --cov-report=html --cov-report=term-missing -
Проанализировать отчёт о покрытии:
- Найти непокрытые строки (например, в
missingколонке). - Дописать тесты, чтобы покрыть их.
- Убедиться, что итоговое покрытие >= 100% (можно установить порог в pytest.ini).
- Найти непокрытые строки (например, в
-
Добавить конфигурацию pytest:
# pytest.ini [pytest] addopts = --cov=tools --cov-report=term-missing --cov-fail-under=100
Ожидаемый результат этапа Все инструменты покрыты тестами, команда pytest завершается успешно с coverage >= 100%.
Этап 5: Интеграция с CI и документирование (30–40 минут)
Действия
-
Добавить workflow для GitHub Actions в
.github/workflows/test.yml:name: Unit Tests on: [push, pull_request] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: '3.10' - run: pip install -r requirements-dev.txt - run: pytest --cov=tools --cov-report=term-missing --cov-fail-under=100 -
Написать README-раздел:
- Как запустить тесты локально.
- Как интерпретировать отчет о покрытии.
- Как добавить новый инструмент и его тесты.
-
Проверить, что CI падает при покрытии ниже 100%.
Ожидаемый результат этапа Репозиторий автоматически проверяет покрытие при каждом Push.
5. Критерии приемки (Definition of Done)
- Все инструменты (как минимум 3) покрыты unit-тестами.
- Каждый тест изолирован: не требует реального API, БД или файловой системы.
- Покрытие строк (line coverage) каждого файла инструментов >= 100%.
- Покрытие веток (branch coverage) также >= 100% (проверить опцией
--cov-branch). - Тесты проходят локально одной командой
pytest. - Настроен CI/CD с проверкой покрытия; PR не мержится при падении.
- Написана документация по запуску и добавлению новых тестов.
- Отсутствуют моки, которые не используются (dead mock) – проверено
pytest --dead-mocks(если установлен плагин). - Все граничные случаи (пустые входные, None, некорректные типы) покрыты.
- Тесты для инструментов с внешними вызовами используют
patchна уровне модуля или объекта.
6. Ожидаемый результат
Основной артефакт Директория tests/ с файлами вида:
tests/
├── conftest.py
├── test_calculator.py
├── test_weather.py
├── test_database_search.py
└── test_email_sender.py
Содержимое
- Каждый файл тестирует один инструмент (или группу связанных).
- Все моки вынесены в фикстуры или декораторы.
- Параметризация для проверки множества входных данных (использовать
@pytest.mark.parametrize).
Дополнительные результаты
7. Возможные сложности и их решение
| Сложность | Решение |
|---|---|
Инструмент использует асинхронные вызовы (async/await) | Использовать pytest-asyncio и mock для асинхронных функций (например, AsyncMock). |
Внешняя библиотека не мокается стандартным patch (например, C-расширение) | Использовать unittest.mock на уровне объекта, а не модуля; или написать адаптер (wrapper). |
| Инструмент имеет side-effect, который нельзя откатить (например, запись в продакшн БД) | Создать отдельную тестовую конфигурацию с временной БД (SQLite in-memory) и переопределить настройки через fixture. |
| Покрытие веток показывает 100%, но есть непроверенные комбинации параметров | Использовать hypothesis для property-based testing и генерации случайных входов. |
| Не получается замокать вызов внутри сложного стека (например, библиотека делает внутренний import) | Мокать на уровне 'package.module.ClassName.method'; использовать patch.object. |
| Инструмент использует глобальные переменные или синглтоны | Вынести конфигурацию в параметры инструмента, переопределять через fixture с monkeypatch. |
8. Бюджет времени (оценка)
| Этап | Время |
|---|---|
| Этап 1: Анализ и окружение | 20–30 минут |
| Этап 2: Тесты первого инструмента | 1–2 часа |
| Этап 3: Изоляция зависимостей | 1–1.5 часа |
| Этап 4: 100% покрытие остальных инструментов | 2–4 часа |
| Этап 5: CI и документация | 30–40 минут |
| Итого | ~5–9 часов |
Примечание Для первого раза с изучением mocking и coverage может потребоваться до 8 часов. С опытом — 3–4 часа.
9. Связанные вопросы из базы знаний
| Вопрос | Тема |
|---|---|
| 42 | Pytest fixtures и conftest |
| 87 | Mocking в Python: unittest.mock vs pytest-mock |
| 134 | Coverage: line, branch и condition |
| 211 | Тестирование асинхронного кода |
| 298 | Parameterized tests с pytest |
| 356 | Тестирование внешних API без реального подключения |
| 401 | CI/CD для Python проекта |
| 478 | Property-based testing с Hypothesis |
| 522 | Интеграционные vs unit-тесты для агентов |
| 667 | Изоляция тестов: временные БД и файлы |
10. Чек-лист самопроверки
- Я создал папку
tests/и файлconftest.pyс общими фикстурами. - Для каждого инструмента написан минимум один тест на успешный сценарий.
- Для каждого инструмента написаны тесты на все исключения и граничные случаи.
- Все внешние зависимости заменены на моки (
patchилиMock). - Команда
pytest --cov=tools --cov-branch --cov-fail-under=100завершается с кодом 0. - Я проверил, что тесты работают без доступа к интернету/сети.
- Я добавил GitHub Actions workflow и убедился, что он проходит.
- Я написал README с примером запуска и пояснением структуры.
- Я использовал параметризацию для сокращения дублирования кода.
- Я исключил из coverage сгенерированный код (например,
__init__), если он не несёт логики.