Уровни: unit, интеграционное, системное, приёмочное

Если у команды 400 автотестов, а релиз всё равно катят с замиранием сердца — почти наверняка тесты стоят не на тех уровнях.

Уровень тестирования — это размер куска системы, который проверяется за один прогон: от одной функции до всего продукта глазами заказчика.

Одну и ту же ошибку можно поймать на разных уровнях, но цена будет отличаться в сотни раз. Неверная формула скидки ловится юнит-тестом за 3 миллисекунды и показывает конкретную строку кода. Она же ловится сквозным тестом через браузер за две минуты — и всё, что вы увидите, это «в корзине ожидалось 4500, а показано 4750». Дальше час раскопок: фронт врёт, бэк врёт или база не та.

Четыре уровня

Модульное (unit)

Проверяется одна функция или класс в изоляции. Всё, от чего этот кусок зависит — база, сеть, время, другие сервисы, — подменяется заглушками (стабами и моками). Пишет обычно сам разработчик, вместе с кодом.

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

def discount(total, is_premium):
    if total >= 5000:
        rate = 0.10
    elif is_premium:
        rate = 0.05
    else:
        rate = 0.0
    return round(total * (1 - rate), 2)

cases = [
    (4999, False, 4999.0),   # граница снизу: скидки ещё нет
    (5000, False, 4500.0),   # ровно порог: скидка 10%
    (1000, True,   950.0),   # премиум без порога: скидка 5%
]

for total, premium, expected in cases:
    got = discount(total, premium)
    status = "OK" if got == expected else "FAIL"
    print(f"{status}: total={total}, premium={premium} -> {got} (ожидали {expected})")

Результат:

OK: total=4999, premium=False -> 4999.0 (ожидали 4999.0)
OK: total=5000, premium=False -> 4500.0 (ожидали 4500.0)
OK: total=1000, premium=True -> 950.0 (ожидали 950.0)

В настоящем проекте вместо print будет фреймворк (pytest, JUnit, Jest), но суть та же: вход, вызов, сравнение с ожидаемым. Такие тесты ловят ошибки в ветвлениях и на границах — и не ловят ничего из того, что происходит между модулями.

Интеграционное

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

Что здесь ловится и больше нигде:

  • Несовпадение контрактов: бэкенд начал отдавать price строкой "1200.00", а фронт ждёт число — оба юнит-теста зелёные, каждый на своих моках.
  • Миграции и схема БД: поле переименовали, код обновили, а старый запрос в отчёте остался.
  • Транзакции: заказ создался, а списание не прошло, и откат не отработал.
  • Таймауты и ретраи: внешний платёжный шлюз отвечает 6 секунд, у клиента таймаут 5 — платёж уходит дважды.

Инструменты: поднятая в Docker настоящая база (testcontainers), фейковый HTTP-сервер вместо чужого API (WireMock, mock-server), контрактные тесты (Pact) — они фиксируют «что обещает сервис» и не дают одной стороне тихо поменять формат.

Системное

Проверяется вся собранная система целиком, в окружении, похожем на боевое, через её внешние интерфейсы — UI или публичный API. Это работа тестировщика, а не разработчика.

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

Приёмочное

Цель не «найти баги», а принять решение: берём или не берём. Проверяет заказчик, продакт, бизнес-заказчик, иногда реальные пользователи (закрытая бета). Критерии приёмки согласуются заранее, до разработки, иначе приёмка превращается в спор о вкусах.

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

УровеньКто обычно делаетЧто проверяетСкоростьТипичный найденный дефект
Unitразработчикодну функцию, зависимости замоканымиллисекундыскидка 10% начисляется на сумме 4999 вместо 5000
Интеграционноеразработчик, иногда QAстык модулей, реальную БД и внешние APIсекундысервис отдаёт цену строкой, клиент ждёт число
СистемноеQAвсю систему по сквозным сценариямминутыпосле оплаты не приходит письмо с чеком
Приёмочноезаказчик, продакт, пользователисоответствие бизнес-задачечасы и днисделали ровно по ТЗ, но пользоваться этим нельзя

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

Идею предложил Майк Кон в книге «Succeeding with Agile» (2009). Смысл — в соотношении: чем ниже уровень, тем таких тестов должно быть больше.

          /\
         /  \        E2E / системные — мало (~10%)
        /____\       медленно, хрупко, но правда о продукте
       /      \
      /        \     Интеграционные — среднее число (~20%)
     /__________\    проверяют стыки
    /            \
   /              \  Unit — много (~70%)
  /________________\ быстро, дёшево, точная локализация

Проценты — не догма, а напоминание об экономике. Юнит-тест стоит копейки: написал за 5 минут, гоняешь тысячу раз в день, при падении сразу видно строку. Сквозной тест стоит дорого: писать долго, гонять минуты, при падении сообщение «элемент с селектором .btn-pay не найден» может означать что угодно — от настоящего бага до того, что вёрстку слегка подвинули.

Отсюда практическое правило: опускай проверку на самый низкий уровень, на котором она ещё осмысленна. Валидацию 12 полей формы не нужно проверять двенадцатью сквозными тестами — хватит одного сквозного «форма отправляется» плюс одиннадцати юнитов на валидаторы.

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

Антипаттерн: перевёрнутая пирамида

Её же называют «рожком мороженого»: внизу почти нет юнит-тестов, зато сверху разрастается ком сквозных автотестов и толстая шапка ручной регрессии.

Как выглядит проект в этой стадии:

  • 400 UI-тестов, полный прогон 6 часов, влезает только ночью.
  • Каждый десятый падает случайно (flaky) — из-за таймингов, анимаций, чужих тестовых данных.
  • Красную сборку перезапускают до зелёного. Это входит в привычку.
  • Особо капризные тесты уводят в «карантин» и там забывают.
  • Итог: набор есть, доверия нет. Перед релизом всё равно садятся кликать руками.

Откуда это берётся. Обычно из организационного разрыва: автотесты пишет отдельная группа «автоматизаторов», у которой нет доступа к коду и нет права требовать юнит-тесты от разработчиков. Единственный доступный им уровень — UI. Плюс менеджерская логика: 400 e2e-тестов выглядят в отчёте внушительнее, чем 3000 юнитов.

Чинить это переписыванием всего с нуля бессмысленно. Работает другое: каждый раз, когда падает или мигает сквозной тест, задавайте один вопрос — может ли эта проверка жить уровнем ниже? Проверка расчёта скидки — уровнем ниже. Проверка, что API отдаёт 403 чужому пользователю, — уровнем ниже. Наверху остаётся десяток сценариев, которые действительно должны пройти путь пользователя целиком: вход, покупка, оплата.

Как это работает

В CI уровни включаются с разной частотой, и это прямое следствие их стоимости:

КогдаЧто гоняемБюджет времени
каждый pushunit + быстрые интеграционныедо 5 минут
на pull request+ интеграционные с базой и контрактныедо 15 минут
ночью и перед релизом+ сквозные E2E, нагрузочныечасы

Здесь же живёт идея shift-left — «сдвига влево»: чем раньше по времени найден дефект, тем дешевле он обходится. Ошибка в требованиях, замеченная аналитиком на встрече, стоит одного разговора. Она же, доехавшая до прода, стоит инцидента, отката и извинений перед клиентами.

Частые ошибки

  • Гнаться за покрытием ради цифры. 80% coverage, набитые тестами на геттеры, не защищают ни от чего. Покрытие — индикатор, а не цель.
  • Тесты, проверяющие моки. Замокали всё, что можно, и в итоге тест утверждает, что заглушка возвращает то, что в неё положили. Такой тест зелёный всегда, включая моменты, когда продукт сломан.
  • Пропустить интеграционный слой целиком. Самая дорогая дыра: юниты зелёные, e2e прошли на счастливом пути, а на проде разъехались контракты между сервисами.
  • Не отличать баг от flaky. Если у команды нет статистики падений и правила «мигающий тест чинится или удаляется за спринт», набор гниёт за пару месяцев.
  • Подменять приёмку регрессом. Когда заказчик впервые видит продукт на демо в конце квартала, приёмочное тестирование превращается в переговоры о том, кто виноват.
  • Строить пирамиду ради пирамиды. Пропорции 70/20/10 — ориентир, а не KPI. Для фронтенда трофей может подойти лучше; важен принцип «дорогое — редко, дешёвое — часто».

Итоги

  • Четыре уровня отличаются размером проверяемого куска: функция → стык модулей → вся система → бизнес-результат.
  • Unit пишет разработчик, интеграционные — разработчик и QA, системные — QA, приёмочные — заказчик и пользователи.
  • Чем ниже уровень, тем дешевле тест и тем точнее он показывает место поломки; чем выше — тем ближе к правде о продукте.
  • Опускай проверку на самый низкий уровень, где она ещё имеет смысл.
  • Перевёрнутая пирамида (гора хрупких UI-тестов без фундамента) даёт долгий прогон, flaky-падения и нулевое доверие к зелёной сборке.
  • Интеграционный уровень пропускают чаще всего — и именно там прячутся расхождения контрактов между сервисами.
Проверьте себя
1. Бэкенд начал отдавать цену строкой "1200.00", а фронтенд ждёт число. Юнит-тесты обеих сторон зелёные. На каком уровне такой дефект ловится?
AНа модульном — надо просто добавить ещё юнит-тестов
BНа интеграционном (в том числе контрактными тестами)
CНа приёмочном — заказчик заметит неправильную цену
DТакой дефект автотестами поймать нельзя
2. Что из перечисленного — главный практический вред перевёрнутой пирамиды тестирования?
AUI-тесты не умеют проверять бизнес-логику в принципе
BАвтотесты требуют лицензий и стоят дорого
CПрогон занимает часы, тесты часто мигают, и команда перестаёт доверять зелёной сборке
DРазработчики не могут читать код UI-тестов