Уровни: 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 уровни включаются с разной частотой, и это прямое следствие их стоимости:
| Когда | Что гоняем | Бюджет времени |
| каждый push | unit + быстрые интеграционные | до 5 минут |
| на pull request | + интеграционные с базой и контрактные | до 15 минут |
| ночью и перед релизом | + сквозные E2E, нагрузочные | часы |
Здесь же живёт идея shift-left — «сдвига влево»: чем раньше по времени найден дефект, тем дешевле он обходится. Ошибка в требованиях, замеченная аналитиком на встрече, стоит одного разговора. Она же, доехавшая до прода, стоит инцидента, отката и извинений перед клиентами.
Частые ошибки
- Гнаться за покрытием ради цифры. 80% coverage, набитые тестами на геттеры, не защищают ни от чего. Покрытие — индикатор, а не цель.
- Тесты, проверяющие моки. Замокали всё, что можно, и в итоге тест утверждает, что заглушка возвращает то, что в неё положили. Такой тест зелёный всегда, включая моменты, когда продукт сломан.
- Пропустить интеграционный слой целиком. Самая дорогая дыра: юниты зелёные, e2e прошли на счастливом пути, а на проде разъехались контракты между сервисами.
- Не отличать баг от flaky. Если у команды нет статистики падений и правила «мигающий тест чинится или удаляется за спринт», набор гниёт за пару месяцев.
- Подменять приёмку регрессом. Когда заказчик впервые видит продукт на демо в конце квартала, приёмочное тестирование превращается в переговоры о том, кто виноват.
- Строить пирамиду ради пирамиды. Пропорции 70/20/10 — ориентир, а не KPI. Для фронтенда трофей может подойти лучше; важен принцип «дорогое — редко, дешёвое — часто».
Итоги
- Четыре уровня отличаются размером проверяемого куска: функция → стык модулей → вся система → бизнес-результат.
- Unit пишет разработчик, интеграционные — разработчик и QA, системные — QA, приёмочные — заказчик и пользователи.
- Чем ниже уровень, тем дешевле тест и тем точнее он показывает место поломки; чем выше — тем ближе к правде о продукте.
- Опускай проверку на самый низкий уровень, где она ещё имеет смысл.
- Перевёрнутая пирамида (гора хрупких UI-тестов без фундамента) даёт долгий прогон, flaky-падения и нулевое доверие к зелёной сборке.
- Интеграционный уровень пропускают чаще всего — и именно там прячутся расхождения контрактов между сервисами.