English translation is not available yet. Showing Russian content.

Протестировать delegation paths

ТЕХНИЧЕСКОЕ ЗАДАНИЕ: Протестировать delegation paths

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

Разработать и выполнить тесты для всех путей делегирования (delegation paths) в распределённой системе с mock downstream, fault injection и валидацией trace. Научиться проверять корректность маршрутизации запросов, тайм-аутов, fallback-логики и обработки ошибок при сбоях внешних сервисов.

Ключевой результат Полный набор тестов (≥10 тестовых сценариев), покрывающий все определённые delegation paths с верификацией через OpenTelemetry traces.


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

Что нужноОткуда взять
Документация делегирования (delegation policy, маршруты, fallback)Предыдущее ТЗ / репозиторий проекта, docs/delegation.md
Сервис-делегатор (главный микросервис, принимающий и распределяющий запросы)Исходный код на Python/Go/Java (реальный или заглушка)
Downstream-сервисы (3-5 шт.)Mock-реализации (например, на unittest.mock, moto, wiremock или встроенные заглушки)
OpenTelemetry collector / трейсинг (Jaeger, Zipkin или in-memory exporter)Установленный OTEL SDK + collector (локальный Docker)
Тестовые запросы (payload) и скрипты генерацииСкрипт tests/generate_requests.py (создаётся самостоятельно)
Метрики успеха: покрытие путей (edge coverage), средняя задержка, код возвратаPrometheus / OpenMetrics или логи

Если нет реального инструмента — симулируем:

  1. Delegation сервис: написать простой Python-сервис на FastAPI с роутером, который на основе заголовка X-Delegation-Path направляет запрос к одному из трёх mock downstream (mock_a, mock_b, mock_c). Включить тайм-ауты (2 с), fallback (если A не ответил — идти к B), retry (1 попытка).
  2. Mock downstream: использовать asyncio и unittest.mock.AsyncMock для симуляции ответов, задержек, ошибок (500, 503, тайм-аут). Упаковать в отдельный модуль mocks/.
  3. Trace validation: настроить OpenTelemetry (TracerProvider, SimpleExportSpanProcessor с консольным экспортером или Jaeger в Docker). Каждый путь delegations должен создавать спаны с атрибутами delegation.path, downstream.name, error.type.
  4. Покрытие путей: собрать карту путей из документации (например: path1: direct A; path2: A→fallback B; path3: A→retry→fallback C и т.д.) и явно задать в тестах.

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

КомпонентИнструментыНазначение
Сервис-делегаторPython (FastAPI) + asyncioОсновной сервис под тестирование
Mock downstreamunittest.mock, aiohttp / httpx (mock backend)Эмуляция внешних сервисов
Тестированиеpytest, pytest-asyncio, pytest-html (отчёты)Запуск тестов, assertions
ТрейсингOpenTelemetry SDK для Python, Jaeger (консоль или Docker)Валидация trace структуры
Контейнеризация (опционально)Docker, docker-composeЗапуск Jaeger, mock-сервисов
CI (опционально)GitHub Actions + pytest-coverageАвтоматический прогон тестов

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

Этап 1: Подготовка окружения и mock-сервисов (2 часа)

Действия

  1. Создать репозиторий (если нет): git init delegation-tests; cd delegation-tests

  2. Установить зависимости: в requirements.txt: fastapi uvicorn pytest pytest-asyncio httpx opentelemetry-api opentelemetry-sdk opentelemetry-instrumentation-fastapi

  3. Реализовать mock downstream сервис (файл mocks/downstream_mock.py):

    import asyncio
    from fastapi import FastAPI
    import random
    
    app = FastAPI()
    
    @app.get("/downstream/{name}")
    async def downstream(name: str, delay: float = 0.0, fail_prob: float = 0.0):
        await asyncio.sleep(delay)
        if random.random() < fail_prob:
            raise HTTPException(status_code=503, detail="Service Unavailable")
        return {"status": "ok", "name": name, "delay": delay}
    
  4. Создать конфигурацию делегирования (delegation_config.py):

    DELEGATION_PATHS = {
        "direct_a":      {"downstream": "downstream_a", "fallback": None, "retry": 0, "timeout": 2.0},
        "a_with_retry":  {"downstream": "downstream_a", "fallback": None, "retry": 1, "timeout": 2.0},
        "a_to_b_fallback": {"downstream": "downstream_a", "fallback": "downstream_b", "retry": 0, "timeout": 2.0},
        "a_retry_to_c":  {"downstream": "downstream_a", "fallback": "downstream_c", "retry": 1, "timeout": 1.0},
        "b_direct":      {"downstream": "downstream_b", "fallback": None, "retry": 0, "timeout": 2.0},
    }
    
  5. Реализовать главный сервис-делегатор (delegator.py):

    • Эндпоинт POST /delegate с телом {"path": "direct_a", "payload": {...}}
    • Логика: по path из DELEGATION_PATHS выбрать downstream, выполнить запрос через httpx.AsyncClient с таймаутом; при ошибке (500, 503, timeout) — применить retry, затем fallback.
    • Интеграция OTel: создать tracer, для каждого вызова создать span delegate_step с атрибутами.
  6. Запустить mock сервисы (по одному процессу на каждый downstream, или через docker-compose с разными портами: 8001, 8002, 8003). В целях упрощения — запустить на разных портах в одном процессе, используя threading или asyncio.

Ожидаемый результат этапа Работающий delegator на порту 8000, три мока на портах 8001–8003, конфигурация путей.


Этап 2: Настройка тестирования и сценариев fault injection (2–3 часа)

Действия

  1. Создать структуру тестов tests/test_delegation.py.

  2. Реализовать фикстуры pytest:

    • delegator_clienthttpx.AsyncClient, указывающий на http://localhost:8000
    • mock_downstreams — запуск трёх отдельных uvicorn процессов для моков в pytest сессии (или использовать mock.patch для перехвата HTTP-запросов внутри декоратора — проще: заменить реальные запросы на моки через httpx_mock).
    • В примере используем pytest-httpx для мокирования исходящих запросов из delegator.
  3. Подготовить функции fault injection:

    • mock_response(status=200, json={"status": "ok"})
    • mock_timeout(delay=3.0) (превышает таймаут delegator)
    • mock_error(status=503)
    • mock_partial_failure(first_fail=True, second_ok=True) для retry
  4. Написать матрицу тестовых сценариев (таблица в коде SCENARIOS):

    СценарийpathDownstream ADownstream BDownstream CОжидаемый код ответаОжидаемая последовательность вызовов
    1. direct_a успехdirect_a200, 0s200[A]
    2. direct_a 500direct_a500502 (или fallback none -> ошибка)[A]
    3. a_with_retry первый неуспехa_with_retry503, 2s; затем 200200[A, A]
    4. a_to_b_fallback A падаетa_to_b_fallbacktimeout (3s)200, 0s200[A, B]
    5. a_retry_to_c A таймаут + retry + fallbacka_retry_to_ctimeout (1.5s)200, 0s200[A, A, C]
    6. a_retry_to_c все падаютa_retry_to_ctimeout (1.5s)503502[A, A, C]
    7. b_direct успехb_direct200200[B]
    8. неизвестный путьunknown404none
    9. все моки упали + retry + fallback пустdirect_a (без fallback)все 503502[A] (retry? path без retry)
    10. timeout на fallbacka_to_b_fallbacktimeout (3s)timeout (3s)504[A, B]
  5. Реализовать параметризованные тесты с использованием @pytest.mark.parametrize.

  6. Добавить проверку trace: после выполнения запроса извлекать из OTel TracerProvider все завершённые спаны через SpanExporter.get_finished_spans() и проверять атрибуты.

Ожидаемый результат этапа Написанные тесты для 10 сценариев + функция проверки trace.


Этап 3: Запуск тестов, отладка и доработка покрытия (2 часа)

Действия

  1. Запустить delegator и mock-сервисы (если не сделано ранее) в отдельных терминалах или docker-compose.
  2. Выполнить тесты: pytest -v tests/
  3. Исправить найденные баги:
    • Неправильная обработка таймаутов
    • Отсутствие fallback при пустом fallback
    • Некорректное поведение retry после fallback
  4. Добавить пропущенные сценарии:
    • Тест с большим количеством retry (например, 3)
    • Тест с параллельными запросами на одинаковые пути
    • Тест на то, что trace не теряется при ошибке
  5. Проверить покрытие edge coverage:
    • Написать скрипт coverage_report.py, который сравнивает выполненные тесты с полным списком путей из конфига + комбинации ошибок (все возможные комбинации фолбэков). Довести покрытие до 100% по сценариям.
  6. Задокументировать coverage в COVERAGE.md.

Ожидаемый результат этапа Все тесты проходят (зелёные), покрытие путей 100%.


Этап 4: Интеграция trace validation и отчётность (1,5 часа)

Действия

  1. Углубить проверку trace:
    • Для каждого теста проверить количество спанов, атрибуты delegation.path, downstream.name, error.type (если была ошибка). Использовать opentelemetry.sdk.trace.export.in_memory.InMemorySpanExporter.
    @pytest.fixture
    def span_exporter():
        exporter = InMemorySpanExporter()
        tracer_provider = TracerProvider()
        tracer_provider.add_span_processor(BatchSpanProcessor(exporter))
        # set as global tracer provider
        trace.set_tracer_provider(tracer_provider)
        return exporter
    
  2. Реализовать ассерт для trace:
    def assert_trace(span_exporter, expected_spans):
        spans = span_exporter.get_finished_spans()
        assert len(spans) == len(expected_spans)
        for span, expected in zip(spans, expected_spans):
            assert span.attributes.get("downstream.name") == expected["downstream"]
    
  3. Сформировать HTML-отчёт (pytest-html): pytest --html=report.html tests/
  4. Добавить метрики (опционально): эмитировать Prometheus-метрики delegation_requests_total, delegation_duration_seconds и проверить их через тестовый экспорт.

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


Этап 5: Документация и ревью (1 час)

Действия

  1. Написать README.md: как запустить тесты, как добавить новый path.
  2. Включить таблицу с результатами тестирования delegation paths (пройден/не пройден, комментарии).
  3. Добавить диаграмму путей (Mermaid) в docs/delegation_paths.md.
  4. Провести peer review: попросить коллегу проверить сценарии и код.

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


5. Критерии приемки (Definition of Done)

  • Все определённые delegation paths покрыты тестами (≥10 сценариев).
  • Каждый тест проверяет код ответа, тело ответа и последовательность вызовов downstream.
  • Реализована fault injection (таймауты, ошибки 5xx, частичные успехи).
  • Trace validation: каждый тест проверяет количество спанов и атрибуты.
  • Тесты могут быть запущены одной командой (pytest -v).
  • Покрытие edge coverage для путей (все комбинации fallback + retry) не менее 90% (цель 100%).
  • Создан отчёт pytest-html с деталями.
  • Написана документация: README, карта путей, конфиг делегирования.
  • В CI (GitHub Actions) добавлен шаг запуска тестов.
  • Воспроизводимость: тесты работают без внешних зависимостей (все моки локальны).

6. Ожидаемый результат

  • Файл/артефакт: репозиторий delegation-tests с исходным кодом delegator (если создавался) и тестами.
  • Содержание:
    • delegator.py — сервис FastAPI (если не был готов).
    • delegation_config.py — конфигурация путей.
    • tests/test_delegation.py — параметризованные тесты.
    • tests/conftest.py — фикстуры (mock, trace exporter).
    • mocks/ — mock downstream (если не симулируются через pytest-httpx).
    • requirements.txt / Pipfile.
    • docs/delegation_paths.md — карта путей (Mermaid).
    • README.md — инструкции.
    • report.html — отчёт pytest.
  • Опционально:
    • Docker-compose для запуска Jaeger и mock-сервисов.
    • GitHub Actions workflow.

7. Возможные сложности и их решение

СложностьРешение
Нет готового delegatorРеализовать минимальный FastAPI сервис как указано в этапе 1.
Синхронизация трейсинга с тестамиИспользовать InMemorySpanExporter и сбрасывать перед каждым тестом.
Тайм-ауты в тестах (ложные срабатывания)Увеличить лимит времени на тест (pytest-timeout), установить разумные задержки моков (< чем таймаут декоратора).
Retry логика и бесконечные циклыЧётко ограничить количество retry в конфиге, проверять счётчики в тестовых assertion.
Неоднородность окружения (порты заняты)Использовать динамические порты (port=0) и фикстуры с pytest-asyncio + uvicorn.
Сложность поддержки большого числа сценариевИспользовать параметризацию со словарём сценариев; добавить автоматическую генерацию из карты путей.

8. Бюджет времени (оценка)

ЭтапВремя (часы)
Подготовка окружения и mock-сервисов2
Настройка тестов и сценариев fault injection3
Запуск тестов, отладка и доработка покрытия2
Интеграция trace validation и отчётность1,5
Документация и ревью1
Итого9,5 часов

Примечание: для первого раза может потребоваться до 16 часов, если делегатор пишется с нуля и не было опыта работы с OTel.


9. Связанные вопросы из базы знаний

ВопросТема
12Что такое delegation path в микросервисной архитектуре
45Как эмулировать отказы downstream (fault injection)
78Интеграция OpenTelemetry span validation в pytest
101Покрытие всех edge-путей в распределённой системе
156Обработка тайм-аутов и retry в asyncio
203Создание mock-сервисов на FastAPI для тестирования
289Использование pytest-httpx для перехвата HTTP-запросов
367Best practices для тестирования fallback-логики
412Написание документации delegation-policy
503Организация CI для тестов с интеграцией трейсинга

10. Чек-лист самопроверки

  • Я корректно настроил OTel tracer и экспортер в тестовых фикстурах.
  • Я проверил, что каждый тест явно указывает ожидаемые вызовы downstream.
  • Я добавил проверку атрибутов span (delegation.path, downstream.name) для всех сценариев.
  • Я убедился, что тесты можно запустить без внешних сервисов (все моки внутри).
  • Я зафиксировал покрытие путей и написал COVERAGE.md.
  • Я проверил, что отчёт pytest-html генерируется и содержит результаты.
  • Я добавил шаг в CI для автоматического прогона тестов.