Тестирование ПО Стратегия автоматизации: пирамида, ROI, что автоматизировать не надо
0%

Стратегия автоматизации: пирамида, ROI, что автоматизировать не надо

Стратегия автоматизации: пирамида, ROI, что автоматизировать не надо

Почти в каждой команде рано или поздно происходит один и тот же разговор. Регресс занимает три дня, релизы задерживаются, кто-то произносит: «нам нужна автоматизация». Через полгода в репозитории лежит 1200 автотестов, регресс всё ещё занимает три дня — потому что теперь нужно разбирать 40 красных тестов, из которых 37 красные не по делу.

Это не история про плохих инженеров. Это история про отсутствие стратегии. Автоматизация — не добродетель и не признак зрелости. Это инвестиция с конкретной стоимостью, конкретным сроком окупаемости и конкретным риском не окупиться никогда. Стратегия автоматизации отвечает ровно на три вопроса: что автоматизировать, на каком уровне и как понять, что мы ошиблись.

Предыдущие статьи курса разбирали, как писать тесты каждого вида — модульные, интеграционные, E2E, API. Эта статья — про то, как из них собрать набор, который стоит своих денег.

Что вы на самом деле покупаете

Первое честное утверждение, которое ломает много ожиданий:

Автотесты почти не находят новых багов.

Исследования и практика совпадают: подавляющее большинство дефектов, которые находит автоматизация, обнаруживаются в момент написания теста — когда человек внимательно читает требования и код. После этого тест годами гоняется зелёным. Новые баги находит исследовательское тестирование, код-ревью и продакшн.

Значит, автоматизация покупает не «поиск багов». Она покупает четыре вещи:

  1. Регрессионный сигнал. Уверенность, что изменение не сломало то, что работало. Это главный и почти единственный продукт автоматизации.
  2. Скорость обратной связи. Разница между «узнал через 40 секунд» и «узнал через три дня» — это разница в стоимости починки на порядок (см. обзор курса).
  3. Право на рефакторинг. Без набора тестов код замерзает: менять страшно.
  4. Освобождение человеческого внимания. Тестировщик, который не проходит вручную 300 регрессионных шагов, тратит эти дни на то, что машина не умеет.

Из этого списка следует критерий полезности теста, который стоит держать в голове постоянно: тест ценен ровно настолько, насколько дорого обойдётся регрессия, которую он ловит, умноженная на вероятность этой регрессии. Тест, проверяющий код, который никто не меняет, не ценен, даже если он идеально написан.

Где расходятся взгляды разработчика и тестировщика

Это первое место в курсе, где две профессии смотрят на одно и то же под разными углами, и различие надо проговорить явно.

Вопрос Взгляд разработчика Взгляд тестировщика
Зачем тест «Чтобы я мог менять код и не бояться» «Чтобы продукт не сломался у пользователя»
Что считается хорошим тестом Быстрый, изолированный, привязан к модулю Проверяет поведение, понятное бизнесу
Что делать с падающим тестом Починить код или тест — сегодня, до мерджа Завести дефект, оценить влияние на релиз
Где ценность Ближе к коду: unit, integration Ближе к пользователю: E2E, API, приёмка
Худший страх Медленный набор, который тормозит работу Дырка в покрытии, через которую утёк баг в прод

Оба взгляда правильные и оба неполные. Разработчик, оставленный один, соберёт быстрый набор, который не проверяет ни одного бизнес-сценария целиком. Тестировщик, оставленный один, соберёт медленный набор сквозных проверок, который падает от каждого рефакторинга. Стратегия автоматизации — это, по сути, договор между этими двумя позициями, зафиксированный в виде формы набора.

Пирамида: что она на самом деле утверждает

Пирамиду тестирования сформулировал Майк Кон в «Succeeding with Agile» (2009), популяризовал Мартин Фаулер в заметке TestPyramid и подробно разобрал Хэм Воке в The Practical Test Pyramid.

Массовое понимание пирамиды — «юнитов должно быть много, E2E мало» — верно по следствию, но неверно по причине. Пирамида утверждает три вещи, и ни одна из них не про количество:

  1. Чем шире охват теста, тем дороже каждый его прогон — по времени, по инфраструктуре, по времени человека на разбор падения.
  2. Чем шире охват, тем хуже локализация. Красный юнит-тест показывает функцию. Красный E2E показывает, что «что-то в системе сломалось» — и дальше час расследования.
  3. Чем шире охват, тем выше вероятность недетерминированного падения. Сеть, тайминги, общие данные, порядок выполнения.

Количество — это вывод: раз верхние уровни дороже за единицу сигнала, покупайте там только тот сигнал, который нельзя купить ниже. Отсюда практическое правило, которое работает лучше любой цифры:

Каждую проверку размещайте на самом низком уровне, на котором она даёт честный ответ. Не ниже — иначе тест проверяет фикцию. Не выше — иначе вы переплачиваете.

«Не ниже» — важная половина, о которой забывают. Юнит-тест с замоканным репозиторием не докажет, что SQL-запрос корректен. Поднимать ради этого E2E тоже не надо — нужен интеграционный тест на реальной БД.

Пирамида — не единственная форма

Форму набора задаёт не мода, а архитектура: там, где живёт сложность, должны жить тесты.

Четыре формы тестового набора: пирамида, рожок мороженого, кубок и соты

  • Пирамида подходит системам с богатой доменной логикой: расчёт тарифов, скоринг, биллинг. Логика в чистых функциях — юнит-тесты дают максимум сигнала за копейки.
  • Кубок (Kent C. Dodds) подходит фронтенду и вообще коду, где почти нет вычислений, а есть композиция: компоненты, роутинг, состояние. Юнит-тест на React-компонент проверяет реализацию, а не поведение; интеграционный тест на «пользователь заполнил форму и увидел результат» — поведение. Плюс широкое основание из статического анализа: типы TypeScript и линтер ловят целый класс дефектов бесплатно.
  • Соты (Spotify) подходят тонким микросервисам, которые сами почти ничего не считают, а склеивают вызовы: юнитить нечего, вся сложность — в интеграциях.
  • Рожок мороженого — единственная форма, которую никто не выбирает осознанно. Она получается сама, когда команда решает «автоматизировать регресс»: берёт готовые ручные тест-кейсы и переводит их в UI-автотесты один к одному.

Последний пункт стоит запомнить как главный антипаттерн стратегии. Автоматизация ручного тест-кейса «как есть» — почти всегда ошибка. Ручной кейс написан для человека, который умеет адаптироваться, замечать боковые странности и понимать контекст. Машина не умеет ничего из этого; ей нужен другой кейс — детерминированный, с явными предусловиями, проверяющий одно утверждение. Перевод «один к одному» даёт самый дорогой и самый хрупкий вариант из возможных.

Отдельно про Google: их знаменитая заметка Just Say No to More End-to-End Tests описывает ровно этот сценарий — команда, где сквозной набор стал настолько ненадёжным, что релиз-инженеры перестали ему верить и начали пропускать красное. Набор тестов, которому не верят, стоит дороже, чем отсутствие набора: вы платите за прогоны и разбор, но решения принимаете без него.

Как решать, что автоматизировать

Шаг 1: отсеять то, что автоматизировать не надо

Честный список кандидатов на «нет» экономит больше, чем любая оптимизация. Не автоматизируйте:

  • Функциональность, которая ещё меняется каждую неделю. Пока экран переделывают, тест на него переписывают. Дождитесь стабилизации: обычно это 2–3 итерации без изменений контракта.
  • Одноразовые проверки. Разовая миграция, проверка гипотезы, «посмотреть, как поведёт себя на этих данных». Прогон будет один — окупаемость невозможна арифметически.
  • Визуальную эстетику и субъективное. «Кнопка выглядит нормально», «текст не режет глаз», «интерфейс понятен». Скриншотное тестирование ловит изменение пикселей, а не уместность — это разные вопросы. Скриншотные тесты имеют смысл только там, где есть дизайн-система и договорённость об эталонах, иначе они превращаются в машину для генерации ложных срабатываний.
  • Сценарии, требующие внешних систем без песочницы. Платёж реальной картой, СМС от живого оператора, интеграция с ведомством, у которого тестового контура нет. Здесь либо контрактные тесты против стаба (интеграционное тестирование), либо ручная проверка на релизе.
  • Исследовательское тестирование. Оно по определению не автоматизируется: ценность в том, что человек придумывает следующий шаг по ходу.
  • То, что уже покрыто ниже. Если расчёт скидки проверен юнит-тестами на 30 комбинациях, E2E «оформить заказ со скидкой» не должен проверять сумму — ему достаточно проверить, что скидка вообще доехала до чека.
  • Редкие сценарии с низким ущербом. Экспорт в экзотический формат, которым пользуются два клиента в квартал, и поломка которого стоит одного звонка в поддержку.

Шаг 2: приоритизировать оставшееся

Работающая эвристика — четыре фактора. Оценивайте каждый от 1 до 5 и считайте.

Фактор Что означает Направление
Ущерб от поломки деньги, репутация, регуляторика, данные больше — выше приоритет
Частота прогона сколько раз в месяц проверка реально нужна больше — выше приоритет
Стабильность объекта как часто меняется UI/контракт под тестом стабильнее — выше приоритет
Стоимость автоматизации часы на написание плюс сложность окружения дешевле — выше приоритет

Первые два дают ценность, вторые два — цену. Приоритет — их отношение.

Правый нижний квадрант — то место, где проекты автоматизации умирают. Дорогая инфраструктура, месяцы работы, и сигнал, который никого не спасает. Обычно туда попадают не по глупости, а по инерции: «нам поручили автоматизировать регресс, в регрессе есть этот кейс, значит автоматизируем».

Шаг 3: решить, на каком уровне

Проверка отобрана — теперь надо выбрать уровень. Это дерево решений закрывает почти все случаи.

Правая ветка с вопросом «есть ли уже покрытие ниже» — самая ценная. Она предотвращает главный источник раздувания E2E-набора: желание «на всякий случай» проверить бизнес-логику ещё и через браузер.

Считаем окупаемость честно

Стоимость владения, а не стоимость написания

Стандартная ошибка оценки: «тест писался 4 часа, ручная проверка занимает 20 минут, значит окупится за 12 прогонов». Так не работает, потому что у автотеста есть постоянные расходы, которых нет у листка с чек-листом.

Полная стоимость владения одним автотестом складывается из:

«Скрытая» ветка не метафора. Если набор идёт 40 минут вместо 8, каждый инженер команды теряет по полчаса на каждой итерации — и начинает мерджить, не дожидаясь. Это реальные деньги, просто они не попадают ни в одну смету.

Простая модель расчёта

Считать окупаемость точно не нужно и невозможно. Нужно отличать «окупится за квартал» от «не окупится никогда». Для этого хватает арифметики.

"""Оценка окупаемости автоматизации одной проверки.

Модель намеренно грубая: цель — не точная цифра, а порядок величины
и ответ на вопрос «стоит ли вообще начинать».
"""
from dataclasses import dataclass


@dataclass
class Candidate:
    name: str
    manual_minutes: float      # сколько минут занимает ручная проверка
    runs_per_month: float      # сколько раз в месяц проверка реально нужна
    write_hours: float         # часы на написание и стабилизацию теста
    maintain_hours_month: float  # часы в месяц на поддержку теста
    triage_minutes: float      # минуты на разбор одного падения
    false_fail_rate: float     # доля прогонов с ложным падением (флак)
    lifetime_months: float     # сколько месяцев функциональность проживёт

    def manual_hours(self) -> float:
        """Накопленные часы ручной проверки за срок жизни."""
        return self.manual_minutes / 60 * self.runs_per_month * self.lifetime_months

    def auto_hours(self) -> float:
        """Накопленные часы автоматизации: написание + поддержка + разбор шума."""
        runs = self.runs_per_month * self.lifetime_months
        triage = runs * self.false_fail_rate * self.triage_minutes / 60
        maintain = self.maintain_hours_month * self.lifetime_months
        return self.write_hours + maintain + triage

    def breakeven_months(self) -> float | None:
        """Через сколько месяцев автоматизация становится дешевле. None — никогда."""
        manual_per_month = self.manual_minutes / 60 * self.runs_per_month
        auto_per_month = (
            self.maintain_hours_month
            + self.runs_per_month * self.false_fail_rate * self.triage_minutes / 60
        )
        gain = manual_per_month - auto_per_month
        if gain <= 0:
            return None  # текущие расходы автотеста выше ручной проверки
        return self.write_hours / gain

    def verdict(self) -> str:
        be = self.breakeven_months()
        if be is None:
            return "НЕ окупится: поддержка дороже ручной проверки"
        if be > self.lifetime_months:
            return f"НЕ успеет окупиться: {be:.1f} мес. при жизни {self.lifetime_months:.0f} мес."
        return f"Окупится за {be:.1f} мес. (экономия {self.manual_hours() - self.auto_hours():.0f} ч)"


candidates = [
    # Стабильный API-тест: дёшево писать, почти не ломается.
    Candidate("Контракт API платежей", manual_minutes=15, runs_per_month=60,
              write_hours=3, maintain_hours_month=0.3, triage_minutes=10,
              false_fail_rate=0.01, lifetime_months=24),
    # E2E через UI по нестабильному экрану: классическая ловушка.
    Candidate("E2E: оформление заказа в UI", manual_minutes=12, runs_per_month=20,
              write_hours=16, maintain_hours_month=3, triage_minutes=25,
              false_fail_rate=0.08, lifetime_months=12),
    # Редкий отчёт: проверять надо раз в месяц, автоматизация бессмысленна.
    Candidate("Экспорт годового отчёта", manual_minutes=25, runs_per_month=1,
              write_hours=10, maintain_hours_month=0.5, triage_minutes=20,
              false_fail_rate=0.05, lifetime_months=36),
]

for c in candidates:
    print(f"{c.name:35} {c.verdict()}")

Вывод:

Контракт API платежей               Окупится за 0.2 мес. (экономия 348 ч)
E2E: оформление заказа в UI         НЕ окупится: поддержка дороже ручной проверки
Экспорт годового отчёта             НЕ успеет окупиться: 55.6 мес. при жизни 36 мес.

Обратите внимание на второй случай. Тест не «плохой» — он проверяет важнейший сценарий. Модель говорит другое: в текущем виде он дороже, чем ручная проверка. Правильный вывод не «не будем автоматизировать оформление заказа», а «сначала снизим стоимость поддержки»: стабильные локаторы, подготовка данных через API, изоляция от внешних сервисов. Уменьшите false_fail_rate до 0.01 и maintain_hours_month до 1 — и тест окупается за полтора месяца.

Точка окупаемости автотеста

Главный вывод из этой картинки: автоматизация окупается не в момент написания теста, а в течение всей его жизни, и наклон кривой поддержки решает всё. Два теста с одинаковой стоимостью написания могут отличаться по итоговой цене в десять раз.

Что делать с этой моделью на практике

Не превращайте её в бюрократию. Достаточно трёх вопросов вслух перед тем, как садиться писать тест:

  1. Сколько раз в месяц эта проверка реально понадобится?
  2. Что должно измениться в продукте, чтобы тест пришлось переписывать? Как часто это происходит?
  3. Если тест упадёт ночью в CI — сколько минут займёт понять, баг это или шум?

Если на третий вопрос ответ «полчаса, надо смотреть видео и логи трёх сервисов» — тест не готов к жизни, независимо от того, насколько важен сценарий.

Уровень как инструмент экономии: пример

Разберём конкретную задачу. Требование: «При заказе от 5000 рублей скидка 10%, от 15000 — 15%; для промо-товаров скидка не применяется; итог округляется до копейки вниз».

Плохая стратегия: один E2E-тест на каждое правило. Шесть тестов по 90 секунд, каждый требует поднятого стенда, каждый падает при редизайне корзины.

Хорошая стратегия — разложить по уровням.

Юнит: вся комбинаторика правил. Здесь живёт настоящая сложность, здесь и покрытие (техники — в тест-дизайне).

import pytest
from decimal import Decimal
from shop.pricing import calculate_total, Item

# Границы классов эквивалентности: 4999 / 5000 / 14999 / 15000.
@pytest.mark.parametrize(
    "amount, expected",
    [
        (Decimal("4999.99"), Decimal("4999.99")),   # ниже порога — без скидки
        (Decimal("5000.00"), Decimal("4500.00")),   # ровно порог — 10%
        (Decimal("14999.99"), Decimal("13499.99")),  # верхняя граница 10%
        (Decimal("15000.00"), Decimal("12750.00")),  # порог 15%
    ],
)
def test_discount_thresholds(amount, expected):
    assert calculate_total([Item(price=amount, promo=False)]) == expected


def test_promo_items_are_excluded_from_discount():
    """Промо-товар не участвует в скидке, но входит в сумму заказа."""
    items = [Item(price=Decimal("9000"), promo=False),
             Item(price=Decimal("2000"), promo=True)]
    # 9000 попадает под 10% -> 8100, промо-2000 без изменений.
    assert calculate_total(items) == Decimal("10100.00")


def test_rounding_is_floor_to_kopeck():
    """Округление вниз: банк не любит копейки в свою пользу."""
    assert calculate_total([Item(price=Decimal("5000.05"), promo=False)]) == Decimal("4500.04")

Шесть проверок, время выполнения — миллисекунды, локализация точная. Сложность по времени линейна от числа позиций в заказе, память константна: это обычный проход по списку, и такие тесты можно множить сотнями без ущерба для сборки.

Интеграционный: правило доехало до персистентного слоя. Проверяем, что скидка сохраняется в заказе и корректно читается — реальная БД, реальный SQL.

E2E: ровно один тест. Не про математику, а про то, что путь целиком жив.

import { test, expect } from '@playwright/test';

test('скидка отображается в корзине и доезжает до чека', async ({ page, request }) => {
  // Подготовка данных — через API, а не через UI: быстрее и на порядок стабильнее.
  const { orderId } = await request
    .post('/api/test/seed-cart', { data: { userId: 'u-42', total: 6000 } })
    .then((r) => r.json());

  await page.goto(`/cart/${orderId}`);

  // Проверяем ФАКТ применения скидки и связку слоёв, а не арифметику:
  // все пороги и округления уже покрыты юнит-тестами.
  await expect(page.getByTestId('discount-badge')).toBeVisible();
  await expect(page.getByTestId('order-total')).not.toHaveText('6 000 ₽');

  await page.getByRole('button', { name: 'Оформить заказ' }).click();
  await expect(page.getByTestId('receipt-discount')).toBeVisible();
});

Итог: шесть E2E по 90 секунд превратились в один E2E, один интеграционный и шесть юнитов. Время прогона упало примерно с девяти минут до полутора, а число мест, которые сломает редизайн корзины, — с шести до одного.

Заметьте деталь в E2E: подготовка корзины идёт через тестовый API-эндпоинт. Это требует доработки приложения — и это нормальная, оправданная инвестиция. Тестируемость — свойство продукта, а не тестов. Если каждый E2E начинается с восьми кликов по UI ради предусловий, вы платите за эти клики в каждом прогоне и получаете восемь дополнительных точек отказа.

Что делать с флаки-тестами

Флаки — тест, который на одном и том же коде иногда зелёный, иногда красный. Это не досадная мелочь, а прямая угроза стратегии: флаки убивают доверие, а без доверия набор бесполезен. Google подробно описывал масштаб проблемы в Flaky Tests at Google; классический разбор причин — у Фаулера в Eradicating Non-Determinism in Tests.

Стратегически важно иметь формальный процесс, а не героизм отдельных людей.

Три правила, которые делают этот процесс работающим:

  1. Карантин всегда со сроком. Карантин без дедлайна — кладбище, куда тесты уезжают навсегда, а покрытие тихо деградирует.
  2. Автоматический ретрай — обезболивающее, а не лечение. retries: 2 в Playwright скрывает симптом. Допустимо как временная мера, обязательно с логированием факта ретрая в метрику: если доля ретраев растёт, набор гниёт.
  3. У теста есть владелец. Тест без владельца не чинят — его перезапускают.

Здесь снова расходятся взгляды: разработчику проще всего удалить мешающий тест, тестировщику — оставить и перезапускать. Оба варианта плохи. Договор должен быть записан в явном виде: критерий карантина, срок, кто владелец, что считается починкой. Подробнее про инфраструктуру карантина и метрики — в следующей статье про CI.

Покрытие как самообман

Покрытие кода (code coverage) — самая любимая руководством и самая опасная метрика автоматизации. Проблема не в самой метрике, а в направлении вывода:

  • Низкое покрытие — валидный сигнал. Если модуль расчёта пенсии покрыт на 12%, это точно повод разобраться.
  • Высокое покрытие ничего не доказывает. Покрытие измеряет, какие строки были исполнены во время прогона, а не какие утверждения были проверены.

Показательный пример: тест ниже даёт 100% покрытия строк и не проверяет ничего.

def test_calculate_total_does_not_crash():
    calculate_total([Item(price=Decimal("5000"), promo=False)])  # ассертов нет

Именно так и появляется «покрытие 85%» в командах, где качество не выросло. Как только покрытие становится целевым показателем, оно перестаёт быть измерением — классический закон Гудхарта. Фаулер об этом писал прямо в TestCoverage: покрытие полезно для поиска непротестированных мест, но бесполезно как цель.

Что делать вместо целевого процента:

  • Смотреть на покрытие дифференциально: покрытие изменённых строк в PR (patch coverage) вместо покрытия всего проекта. Это меняет поведение в нужную сторону, не создавая гонки за цифрой.
  • Использовать мутационное тестирование там, где цена ошибки высока (mutmut, Stryker): оно вносит изменения в код и проверяет, упадёт ли тест. Метрика «выживших мутантов» отвечает на настоящий вопрос — проверяют ли тесты хоть что-нибудь. Это дорого по времени, поэтому применяйте точечно, к ядру домена.
  • Держать в голове главную метрику стратегии: сколько дефектов утекло в прод и на каком уровне их следовало поймать. Это единственная метрика, которая напрямую связана с целью.

Как выглядит стратегия документом

Стратегия автоматизации помещается на одну страницу. Пример для типичного бэкенд-сервиса с веб-интерфейсом:

# docs/testing-strategy.yaml — договор команды, а не бюрократия
уровни:
  unit:
    что: доменная логика, расчёты, валидация, чистые функции
    бюджет_времени: до 90 секунд на весь набор
    запуск: каждый коммит, локально перед пушем
    владелец: разработчик, написавший код
  integration:
    что: репозитории на реальной БД, миграции, публикация в брокер
    инструмент: testcontainers
    бюджет_времени: до 6 минут
    запуск: каждый PR
    владелец: разработчик
  contract:
    что: совместимость версий API между сервисами
    бюджет_времени: до 2 минут
    запуск: каждый PR обеих сторон контракта
    владелец: команда-владелец API
  e2e:
    что: 12 критических пользовательских путей, не больше
    правило: бизнес-расчёты внутри E2E НЕ проверяются
    бюджет_времени: до 12 минут при параллелизации на 4 воркера
    запуск: перед мерджем в main и после деплоя на stage
    владелец: QA-инженер

не_автоматизируем:
  - визуальную эстетику вне дизайн-системы
  - интеграцию с внешним ведомством (нет песочницы) — ручная проверка на релизе
  - экраны в статусе «эксперимент» до стабилизации контракта

флаки:
  критерий_карантина: 2 ложных падения за 30 прогонов
  срок_карантина: 10 рабочих дней
  после_срока: тест удаляется, риск фиксируется в реестре

метрики_здоровья:
  - доля прогонов с ретраем (цель: ниже 1%)
  - медианное время до красного сигнала (цель: ниже 10 минут)
  - число дефектов, утёкших в прод, с разбором уровня-виновника
  - patch coverage в PR (наблюдаем, не гейтим жёстко)

Формально закреплённые бюджеты времени — самая недооценённая часть. Без них набор растёт монотонно: каждый отдельный тест «всего плюс 20 секунд», а через год сборка идёт час. Бюджет превращает добавление теста в осознанный выбор: чтобы добавить, надо либо ускорить существующие, либо удалить лишние.

Соответствующая раскладка по джобам в CI выглядит примерно так — подробный разбор параллелизации и quality gates будет в следующей статье:

# .github/workflows/tests.yml — быстрое отдельно от медленного
name: tests
on: [pull_request]

jobs:
  fast:
    runs-on: ubuntu-latest
    timeout-minutes: 5
    steps:
      - uses: actions/checkout@v4
      - run: make lint typecheck        # статика — самый дешёвый сигнал
      - run: pytest tests/unit -q       # бюджет: 90 секунд

  integration:
    needs: fast                          # нет смысла поднимать БД, если статика красная
    runs-on: ubuntu-latest
    timeout-minutes: 10
    steps:
      - uses: actions/checkout@v4
      - run: pytest tests/integration -q --maxfail=5

  e2e:
    needs: integration
    runs-on: ubuntu-latest
    timeout-minutes: 15
    strategy:
      fail-fast: false
      matrix:
        shard: [1, 2, 3, 4]              # 12 путей на 4 воркера
    steps:
      - uses: actions/checkout@v4
      - run: npx playwright test --shard=${{ matrix.shard }}/4
      - uses: actions/upload-artifact@v4
        if: failure()
        with:
          name: traces-${{ matrix.shard }}
          path: test-results/            # трейсы: без них разбор падения стоит часы

Ключевая идея раскладки — порядок по стоимости сигнала. Статика стоит секунды, юниты — минуту, E2E — минуты и инфраструктуру. Запускать дорогое до дешёвого — значит платить за информацию, которую можно было получить бесплатно. Загрузка трейсов при падении не косметика: она напрямую снижает triage_minutes из модели окупаемости, то есть уменьшает наклон кривой стоимости.

Внедрение с нуля и в легаси

С нуля. Не начинайте с E2E-фреймворка. Порядок, который почти всегда работает: статический анализ → юниты на новый код → интеграционные тесты на слой данных → 2–3 смоук-E2E на критические пути. Каждый следующий шаг делайте, когда предыдущий стабильно зелёный.

В легаси без тестов. Здесь порядок другой, и он контринтуитивен для тех, кто любит пирамиду. Юнит-тестировать легаси-код обычно невозможно без рефакторинга, а рефакторить без тестов страшно — классический замкнутый круг, описанный Майклом Физерсом в «Working Effectively with Legacy Code».

Разрывать его надо сверху: написать несколько широких «характеризационных» тестов (characterization tests), которые фиксируют текущее поведение системы — каким бы оно ни было, включая баги. Это временный рожок мороженого, и это осознанное решение: он даёт страховочную сетку для рефакторинга. По мере того как код становится тестируемым, проверки съезжают вниз, а широкие тесты удаляются.

Отдельно: не создавайте изолированную команду автоматизаторов. Это организационный антипаттерн с предсказуемым исходом — автотесты отстают от разработки, разработчики не считают их своими, а команда автоматизации превращается в бутылочное горлышко. Автотесты живут в том же репозитории, что и код, ревьюятся вместе с ним и чинятся тем, кто сломал. Данные из State of DevOps / Accelerate устойчиво связывают автоматизацию тестов с производительностью команды именно при условии, что тесты поддерживают сами разработчики.

Типичные ошибки

  • Автоматизировать существующие ручные кейсы один к одному. Даёт рожок мороженого и максимальную стоимость поддержки.
  • Целевой процент покрытия как KPI. Порождает тесты без ассертов.
  • «Автоматизируем 100% регресса». Недостижимо и не нужно: часть проверок дешевле делать руками навсегда.
  • Игнорировать стоимость поддержки при оценке. Смета «написать тесты за два месяца» без строки «поддерживать N часов в месяц» — заведомо ложная.
  • Record & playback как основной инструмент. Записанные сценарии не структурированы, дублируют друг друга и ломаются от любого изменения вёрстки. Как способ быстро набросать черновик — годится; как основа набора — нет.
  • Никогда не удалять тесты. Набор надо пропалывать. Тест, который два года не падал и проверяет то же, что три соседних, — это чистый расход.
  • Считать автоматизацию заменой ручному тестированию. Она заменяет только повторяемую часть. Исследовательское тестирование и приёмку она не заменяет никогда.
  • Считать, что стратегия пишется один раз. Архитектура меняется — форма набора должна меняться вместе с ней.

Мини-итог

  • Автоматизация покупает регрессионный сигнал и скорость обратной связи, а не поиск новых багов.
  • Пирамида — не про количество тестов, а про стоимость и качество сигнала. Форму набора диктует архитектура: пирамида, кубок или соты в зависимости от того, где живёт сложность. Рожок мороженого не выбирают — в него сползают.
  • Размещайте проверку на самом низком уровне, где она даёт честный ответ.
  • Решение об автоматизации — это расчёт: ущерб и частота против стоимости написания и, главное, стоимости владения. Наклон кривой поддержки решает больше, чем стоимость написания.
  • Есть большой честный список того, что автоматизировать не надо: нестабильное, одноразовое, субъективное, редкое с низким ущербом, уже покрытое ниже.
  • Флаки-тесты требуют процесса с карантином и сроком, а не героизма.
  • Покрытие полезно как индикатор дырок и вредно как цель. Патч-покрытие и мутационное тестирование честнее общего процента.
  • Стратегия помещается на страницу и обязательно включает бюджеты времени и список «не автоматизируем».

Что дальше

Стратегия определяет, что и на каком уровне автоматизировать. Дальше нужно заставить это работать в конвейере: настроить quality gates так, чтобы они блокировали настоящие проблемы и не блокировали шум, распараллелить прогон, чтобы уложиться в бюджет времени, и построить инфраструктуру для карантина флаки-тестов.

Читайте дальше: Тесты в CI: quality gates, параллелизация, флаки-тесты, покрытие.

Источники

Нашли неточность? Выделите фрагмент текста — рядом появится жучок.

Нужен разбор именно вашей ситуации?

Статья описывает общий случай. Если у вас частный — можно разобрать его отдельно, платно. А если не хватает целого материала, предложите тему: её оплачивают вскладчину, и она выходит открытой для всех.

Доска запросов