中文翻译暂不可用,显示俄语原文。

Реализовать tool testing изолированно

ТЕХНИЧЕСКОЕ ЗАДАНИЕ: Реализовать tool testing изолированно

1. Цель задачи

Научиться unit-тестировать каждый инструмент (tool) AI-агента в изоляции от LLM и внешних зависимостей. Разработать набор тестов, который покрывает все логические ветви каждого инструмента (100% branch coverage) и проверяет корректность работы при нормальных и граничных входных данных.

Ключевой результат Исполняемый pytest-набор с coverage >= 100% по строкам для каждого инструмента, запускаемый одной командой.


2. Исходные данные

Что нужноОткуда взять
Кодовая база агента с инструментамиРепозиторий проекта (например, pet-проект или существующий агент)
Список всех инструментовДокументация или модули tools/
Тестовая среда PythonЛокальная установка Python 3.10+
Библиотека для mockingunittest.mock или pytest-mock
Инструмент для измерения покрытияpytest-cov

Если нет готового проекта с инструментами — симулируем:

  1. Создать простую библиотеку из 3–4 инструментов (например, get_weather, search_database, send_email, calculator).
  2. Каждый инструмент должен иметь как минимум один вызов внешнего API или чтение файла/конфига.
  3. Написать фиктивные реализации внешних сервисов (stub) для тестирования без реального подключения.

3. Технологический стек

КомпонентИнструментыНазначение
Язык программированияPython 3.10+Реализация инструментов и тестов
Тестовый фреймворкpytest 7.xЗапуск и организация тестов
Mockingpytest-mock / unittest.mockИзоляция внешних зависимостей
Покрытие кодаpytest-covИзмерение и отчет по coverage
Фикстурыpytest fixturesПодготовка тестовых данных
CI/CD (опционально)GitHub ActionsАвтоматический прогон тестов

4. Этапы выполнения

Этап 1: Анализ инструментов и установка окружения (20–30 минут)

Действия

  1. Определить список инструментов, подлежащих тестированию

    • Пройтись по коду проекта, выписать все классы/функции, декорированные как @tool или используемые агентом.
    • Для каждого инструмента записать:
      • входные параметры (типы, значения по умолчанию)
      • возможные исключения (ConnectionError, ValueError, FileNotFoundError и т.д.)
      • внешние зависимости (API-вызовы, БД, файловая система)
    • Результат занести в таблицу (например, в Google Sheets или Markdown).
  2. Установить тестовые зависимости

    pip install pytest pytest-cov pytest-mock
    
  3. Настроить структуру тестов

    • Создать папку tests/ на одном уровне с tools/.
    • Создать файл conftest.py для общих фикстур.

Ожидаемый результат этапа Список инструментов с зонами тестирования, готовое окружение.


Этап 2: Написание unit-тестов для первого инструмента (1–2 часа)

Действия

  1. Выбрать простейший инструмент (например, calculator или get_current_time).

  2. Написать тесты на нормальные сценарии:

    • Все обязательные параметры переданы корректно.
    • Типы данных совпадают с ожидаемыми.
    # 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)
    
  3. Написать тесты на граничные случаи:

    • Пустые строки, None, отрицательные числа, переполнение.
  4. Написать тесты на обработку ошибок:

    • Неверный тип аргумента, отсутствие обязательного параметра, внешний сбой.
  5. Запустить тесты и проверить покрытие:

    pytest tests/test_calculator.py --cov=tools.calculator --cov-report=term-missing
    
    • Добиться 100% покрытия строк и веток (если инструмент содержит ветвления).

Ожидаемый результат этапа Полный набор тестов для первого инструмента с 100% coverage.


Этап 3: Изоляция внешних зависимостей (1–1.5 часа)

Действия

  1. Для инструментов, вызывающих внешние 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")
    
  2. Для инструментов, работающих с файловой системой, использовать tmp_path фикстуру pytest или pyfakefs.

  3. Для инструментов с вызовом LLM (например, summarize), замокать LLM-клиент на уровне mocked_client.generate().

  4. Проверить, что тесты проходят без доступа к реальным сервисам.

  5. Добавить фикстуры в 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 часа)

Действия

  1. Для каждого оставшегося инструмента повторить этапы 2 и 3.

  2. Особое внимание уделить:

    • Исключительным путям (except-блоки).
    • Ветвям if-elif-else.
    • Циклам (пустой список, один элемент, много).
    • Конфигурационным параметрам, влияющим на поведение.
  3. Создать общий тестовый раннер:

    pytest tests/ --cov=tools --cov-report=html --cov-report=term-missing
    
  4. Проанализировать отчёт о покрытии:

    • Найти непокрытые строки (например, в missing колонке).
    • Дописать тесты, чтобы покрыть их.
    • Убедиться, что итоговое покрытие >= 100% (можно установить порог в pytest.ini).
  5. Добавить конфигурацию pytest:

    # pytest.ini
    [pytest]
    addopts = --cov=tools --cov-report=term-missing --cov-fail-under=100
    

Ожидаемый результат этапа Все инструменты покрыты тестами, команда pytest завершается успешно с coverage >= 100%.


Этап 5: Интеграция с CI и документирование (30–40 минут)

Действия

  1. Добавить 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
    
  2. Написать README-раздел:

    • Как запустить тесты локально.
    • Как интерпретировать отчет о покрытии.
    • Как добавить новый инструмент и его тесты.
  3. Проверить, что 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).

Дополнительные результаты

  • Отчёт о покрытии в HTML (htmlcov/index.html).
  • README с инструкцией.
  • CI-пайплайн.

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. Связанные вопросы из базы знаний

ВопросТема
42Pytest fixtures и conftest
87Mocking в Python: unittest.mock vs pytest-mock
134Coverage: line, branch и condition
211Тестирование асинхронного кода
298Parameterized tests с pytest
356Тестирование внешних API без реального подключения
401CI/CD для Python проекта
478Property-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__), если он не несёт логики.