Modern QA2026Тестирование в продакшене с помощью feature-флагов
Join

Course06 Observability-Driven Testing

Cutting-edge · Chapter 06

Тестирование в продакшене с помощью feature-флагов

Updated Jul 2026

Основная идея

Feature-флаги отделяют деплой от релиза. Вы деплоите код в продакшен, но контролируете, кто его видит. Это превращает каждый деплой в тестируемый эксперимент с измеримыми результатами и автоматическим откатом.

Традиционный подход — деплоить всё для всех одновременно — это бинарная ставка. Feature-флаги превращают эту ставку в серию небольших обратимых экспериментов.

Платформы feature-флагов

Платформа Тип Таргетирование Аналитика Стоимость
LaunchDarkly SaaS Пользователь, сегмент, %-базированное, кастомные правила Встроенное экспериментирование SaaS за пользователя
Unleash Open source / SaaS На основе стратегий (постепенное, по ID пользователя, по IP) Базовые метрики Бесплатно (self-hosted)
Flagsmith Open source / SaaS Сегмент, %-базированное, мультивариантное Встроенная аналитика Бесплатный тариф
Split SaaS Таргетирование на основе атрибутов Полный набор экспериментирования SaaS за пользователя
OpenFeature Стандарт/SDK Независимая от провайдера спецификация Зависит от провайдера Бесплатно (спецификация)

Выбор платформы

  • LaunchDarkly для корпоративных команд, которым нужны наиболее зрелые возможности таргетирования и экспериментирования
  • Unleash для команд, которым нужен open-source, self-hosted контроль с поэтапными стратегиями выпуска
  • Flagsmith для команд, которым нужен хороший баланс функций и цены с open-source вариантом
  • OpenFeature как абстрактный слой, если вы хотите избежать привязки к вендору или использовать несколько провайдеров

Стратегия тестирования feature-флагов

Паттерн выпуска с контролем качества

# Feature-flagged AI summarization with quality gates and fallback
import ldclient
from ldclient.config import Config
from ldclient import Context

ldclient.set_config(Config("sdk-key-production"))
client = ldclient.get()

def get_ai_summary(document, user_context):
    """Feature-flagged AI summarization with automatic quality gate."""
    context = Context.builder(user_context["user_id"]) \
        .set("plan", user_context["plan"]) \
        .set("region", user_context["region"]) \
        .build()

    # Check if this user should get the new AI summary feature
    if client.variation("ai-summary-v2", context, False):
        try:
            summary = call_new_ai_summary_endpoint(document)

            # Quality gate: verify the summary meets minimum quality bar
            quality_score = evaluate_summary_quality(summary, document)
            if quality_score < 0.7:
                # Track degraded quality as a metric
                client.track("ai-summary-quality-degraded", context,
                           metric_value=quality_score)
                # Fall back to old implementation
                return call_legacy_summary_endpoint(document)

            client.track("ai-summary-v2-success", context,
                       metric_value=quality_score)
            return summary

        except Exception as e:
            client.track("ai-summary-v2-error", context)
            return call_legacy_summary_endpoint(document)
    else:
        return call_legacy_summary_endpoint(document)

План поэтапного выпуска

Дисциплинированный выпуск проходит через шлюзы, каждый с конкретными сигналами качества:

# progressive-rollout-plan.yaml
feature: ai-summary-v2
rollout_stages:
  - name: internal_dogfood
    percentage: 0%
    targeting: "email ends with @ourcompany.com"
    duration: 3 days
    quality_gates:
      - error_rate < 1%
      - p95_latency < 3s
      - quality_score_avg > 0.8
    rollback_trigger: "any gate fails for 15 minutes"

  - name: beta_users
    percentage: 5%
    targeting: "plan == 'beta'"
    duration: 5 days
    quality_gates:
      - error_rate < 0.5%
      - p95_latency < 2.5s
      - quality_score_avg > 0.82
      - user_satisfaction_score > 4.0
    rollback_trigger: "any gate fails for 30 minutes"

  - name: gradual_rollout
    percentage_stages: [10%, 25%, 50%, 75%, 100%]
    advance_interval: 24 hours
    quality_gates:
      - error_rate < 0.3%
      - p95_latency < 2s
      - quality_score_avg > 0.85
      - no_pager_incidents
    rollback_trigger: "any gate fails for 1 hour OR pager fires"

  - name: general_availability
    percentage: 100%
    cleanup: "remove feature flag, delete old code path"

Тестирование поведения feature-флагов

Feature-флаги сами по себе нуждаются в тестировании. Неправильно настроенный флаг может вызвать частичный сбой или несогласованный пользовательский опыт.

Модульное тестирование логики флагов

# test_feature_flags.py
import pytest
from unittest.mock import patch

class TestFeatureFlagBehavior:
    def test_flag_on_uses_new_implementation(self, mock_ld_client):
        """When flag is ON, the new implementation should be used."""
        mock_ld_client.variation.return_value = True

        result = get_ai_summary("test document", {"user_id": "u1", "plan": "beta"})

        assert result.source == "ai-summary-v2"
        mock_ld_client.track.assert_called_with(
            "ai-summary-v2-success", pytest.ANY, metric_value=pytest.ANY
        )

    def test_flag_off_uses_legacy_implementation(self, mock_ld_client):
        """When flag is OFF, the legacy implementation should be used."""
        mock_ld_client.variation.return_value = False

        result = get_ai_summary("test document", {"user_id": "u1", "plan": "free"})

        assert result.source == "legacy-summary"

    def test_flag_on_with_quality_degradation_falls_back(self, mock_ld_client):
        """When flag is ON but quality is poor, should fall back to legacy."""
        mock_ld_client.variation.return_value = True

        with patch("evaluate_summary_quality", return_value=0.4):
            result = get_ai_summary("test document", {"user_id": "u1", "plan": "beta"})

        assert result.source == "legacy-summary"
        mock_ld_client.track.assert_called_with(
            "ai-summary-quality-degraded", pytest.ANY, metric_value=0.4
        )

    def test_flag_on_with_exception_falls_back(self, mock_ld_client):
        """When flag is ON but the new implementation throws, should fall back."""
        mock_ld_client.variation.return_value = True

        with patch("call_new_ai_summary_endpoint", side_effect=TimeoutError):
            result = get_ai_summary("test document", {"user_id": "u1", "plan": "beta"})

        assert result.source == "legacy-summary"
        mock_ld_client.track.assert_called_with("ai-summary-v2-error", pytest.ANY)

Интеграционное тестирование: оба пути

Каждый путь кода с feature-флагом должен быть покрыт интеграционными тестами:

@pytest.mark.parametrize("flag_state", [True, False])
def test_summary_endpoint_works_in_both_states(flag_state, test_client, mock_flags):
    """Both code paths must produce valid responses."""
    mock_flags.set("ai-summary-v2", flag_state)

    response = test_client.post("/api/summarize", json={"document": "Test content..."})

    assert response.status_code == 200
    assert "summary" in response.json()
    assert len(response.json()["summary"]) > 0

Гигиена feature-флагов

Управление техническим долгом

Feature-флаги, оставшиеся в коде навсегда, становятся техническим долгом. Внедрите процесс очистки:

Возраст флага Действие
< 7 дней Активный выпуск — оставить как есть
7-30 дней Должен быть на 100% или откачен
30-90 дней Запланировать задачу на очистку
> 90 дней Флаг является техническим долгом — приоритизировать удаление

Соглашения об именовании флагов

Последовательное именование делает флаги обнаруживаемыми и их назначение понятным:

{team}-{feature}-{version}

Examples:
  checkout-ai-summary-v2
  search-semantic-ranking-v1
  onboarding-new-flow-q1-2026

Мониторинг воздействия feature-флагов

Каждый feature-флаг должен иметь связанные метрики, отвечающие на вопросы:

  1. Работает ли новый путь кода? (Доля ошибок по состоянию флага)
  2. Производителен ли он? (Задержка по состоянию флага)
  3. Лучше ли он для пользователей? (Бизнес-метрики по состоянию флага)
  4. Стабилен ли он? (Нет тренда деградации со временем)
# Prometheus metrics for feature flag monitoring
from prometheus_client import Counter, Histogram

flag_requests = Counter(
    'feature_flag_requests_total',
    'Total requests per feature flag state',
    ['flag_name', 'flag_state', 'outcome']
)

flag_latency = Histogram(
    'feature_flag_latency_seconds',
    'Latency by feature flag state',
    ['flag_name', 'flag_state'],
    buckets=[0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0]
)

Feature-флаги — это фундамент тестирования в продакшене. Они превращают деплой из рискового события в контролируемый эксперимент с измеримыми результатами качества.