1. Смотрим не на название, а на две оси
В разговорах о тестировании легко запутаться уже на уровне слов. В одной команде «integration test» — это почти любой тест со Spring. В другой — только live-server через реальный HTTP. Поэтому полезнее опираться не на название файла, а на две практические оси: что в тесте реально работает и что этим тестом вообще можно доказать.
Первая ось — объём реального окружения. Запускаете ли вы один Java-объект в памяти? Поднимаете ли web-слой? Поднимаете ли полноценный Spring context? Используете ли реальный HTTP-сервер? Добавляете ли реальный PostgreSQL в контейнере? Чем больше реального мира вы делаете частью теста, тем выше цена запуска и тем больше потенциальных мест, где тест может сломаться не по той причине, которую вы сейчас исследуете.
Вторая ось — зона доказательства. Одни тесты лучше всего ловят локальное правило, другие — контракты HTTP-границы, третьи — wiring между слоями, четвёртые — нюансы реального окружения. Если держать эти две оси в голове, сразу становится проще: вы выбираете не «модный» тест, а адекватную глубину проверки для конкретного риска.
2. Unit-тесты: самая дешёвая защита локального смысла
Unit-уровень — это место, где вы проверяете кусок поведения без Spring, без базы, без сети и без случайной инфраструктуры вокруг. Такой тест почти всегда быстрый, точный и дешёвый в диагностике. Если он падает, причину обычно можно понять за секунды, а не идти разбираться в половине приложения.
В ContentHub естественные цели для unit-проверок — это policy-объекты, маленькие доменные правила, генерация slug, ограничения переходов между статусами, локальные преобразования данных. Всё то, что можно честно сделать наблюдаемым как обычную Java-логику. Например, правило «approve разрешён только из IN_REVIEW» прекрасно живёт на этом уровне.
// Policy-объект: инкапсулирует доменное правило (локальная логика без Spring/БД/HTTP)
class PublicationPolicy {
// Проверяем, можно ли выполнить approve из текущего статуса статьи
boolean canApprove(ArticleStatus status) {
// Единственное разрешённое состояние для approve — IN_REVIEW
// Любой другой статус (например DRAFT) должен возвращать false
return status == ArticleStatus.IN_REVIEW;
}
}
Такой код не нуждается в ApplicationContext, MVC и реальной базе, чтобы обнаружить регрессию. Если завтра кто-то «для ускорения процесса» разрешит approve из DRAFT, unit-тест должен поймать это мгновенно. И это сильный аргумент в пользу unit-уровня: здесь вы ловите поломку там, где она родилась, а не после долгой цепочки косвенных эффектов.
Но важно быть честным до конца. Unit-тест не доказывает, что HTTP endpoint вызывает это правило правильно. Не доказывает, что данные сохранились в базе. Не доказывает, что security не пускает лишнего пользователя. Он защищает локальный смысл. И именно поэтому он так ценен: он не обещает лишнего.
3. Slice-тесты: реальная инфраструктура одного слоя
Как только риск уходит с уровня «чистое правило» на уровень HTTP, JSON или persistence, unit-подход начинает либо недокрывать проблему, либо вырождаться в театр моков. Именно здесь в экосистеме Spring Boot особенно полезна идея test slices: вы поднимаете не всё приложение, а контролируемый срез, в котором реальной становится инфраструктура конкретного слоя.
У slice-тестов важная философия. Это не «недоинтеграция» и не компромисс от бедности. Это нормальный способ проверить слой честно и быстро. Для web-слоя важно, чтобы реально работали маршрутизация, binding параметров, validation, сериализация и перевод ошибок. Для JSON-контрактов важно реальное поведение сериализатора. Для слоя данных важны запросы, маппинги, ограничения и вообще поведение этого слоя. Во всех этих случаях поднимать полный backend необязательно — и часто просто вредно.
Поэтому slice-уровень в этом курсе будет огромной частью ежедневной практики. Он учит очень полезной дисциплине: делать реальным ровно тот кусок инфраструктуры, где живёт риск, а всё остальное не тащить без причины. Это один из самых профессиональных навыков в обычном Spring Boot-проекте.
4. Integration-тесты: когда нужно проверить стык, а не только слой
Интеграционный тест начинается там, где поломка живёт не внутри одного слоя, а на соединении нескольких частей. Контроллер вызывает сервис, сервис работает с репозиторием, рядом подключены security-фильтры, обработка ошибок, конфигурация, сериализация — и вам важно увидеть, как всё это ведёт себя вместе. Здесь уже недостаточно честного slice-теста, потому что интересующий риск родился именно на стыке.
В Spring Boot-проектах слово integration часто связывают с полным контекстом приложения. Это нормальная ассоциация, но здесь важно не название аннотации, а мысль: вы запускаете достаточно широкий кусок системы, чтобы поймать wiring, конфигурацию, перевод исключений, цепочку controller → service → repository и другие совместные эффекты.
Цена у этого уровня тоже честная. Он дольше запускается, шумнее падает и хуже локализует причину сбоя. Поэтому integration-тесты ценны не своей шириной как таковой, а тем, что они закрывают именно те риски, которые действительно живут между частями системы. Как только вы забываете это условие, начинается злоупотребление полным контекстом.
5. Live-server: когда важен настоящий HTTP-обмен
Есть ещё более широкий шаг: тест, который идёт не через локальный вызов web-слоя, а через реально поднятый HTTP-сервер. С точки зрения разработчика это очень важная граница. Внутрипроцессный web-тест и live-server проверяют похожие сценарии, но не одну и ту же реальность. Во втором случае приложение ведёт себя ближе к настоящему серверу, а тест общается с ним как внешний клиент.
Что это добавляет? Во-первых, уверенность в реальном HTTP round-trip: сервер действительно поднялся на порту, запрос реально ушёл и вернулся ответ. Во-вторых, лучше становятся видны некоторые детали транспортного поведения, фильтров и серверной конфигурации. В-третьих, этот режим очень хорошо показывает, что клиент «снаружи» действительно увидит ту систему, которую вы обещаете, а не только корректный вызов внутри JVM.
Но live-server нельзя делать режимом по умолчанию для всего набора тестов. Он дорогой. Если вы проверяете локальное правило workflow, live-server — это просто медленная декорация. Его сила раскрывается только там, где вопрос действительно звучит так: «а что увидит реальный HTTP-клиент?»
6. Container-тесты: поведение зависит от инфраструктуры
Контейнерные тесты нужны не затем, чтобы сделать всё «ещё более серьёзным», а затем, чтобы перестать врать себе насчёт поведения внешнего компонента. Для ContentHub главным примером здесь будет реальный PostgreSQL. На упрощённом уровне данные могут выглядеть нормально, а на реальной базе внезапно проявляются диалект, constraints, timestamps, сортировки и миграции, которые нельзя честно симулировать ни в голове, ни в случайной тестовой замене.
Поэтому container-тест — это не «последний и самый правильный уровень». Это отдельный инструмент для отдельной категории риска: когда вам важно поведение конкретной инфраструктуры. В таком тесте вы платите временем старта и дополнительной сложностью окружения, зато получаете гораздо более честную картину там, где реальная среда действительно меняет результат.
Именно поэтому в зрелой стратегии container-path обычно появляется позже и точечно. Не как замена всем остальным тестам, а как усилитель для особенно чувствительных сценариев. Это важная мысль для всего курса: дороже не значит лучше. Дороже значит «стоит применять там, где есть дополнительная ценность» 💡
7. Регрессионное тестирование
Есть ещё одно слово, которое часто хочется поставить в один ряд с unit и integration, — regression. Но регрессионный набор лучше понимать не как отдельную технологию, а как специально отобранную часть всего test suite. Его смысл не в том, чтобы быть самым большим или самым тяжёлым. Его смысл — давать максимум пользы за вменяемое время запуска.
В нормальном backend-проекте regression suite почти всегда смешанный. В него попадают быстрые и ценные unit-тесты, пара ключевых slice-проверок, несколько дорогих, но очень важных интеграционных сценариев. Иногда туда входят и live-server-проверки, если цена оправдана. А вот container-тесты чаще остаются в более позднем и узком поднаборе, потому что они существенно дороже в повседневном прогоне.
Это полезно понять уже сейчас: хороший suite — это не куча одинаково широких тестов. Это композиция разных уровней, где каждый платит за себя понятной пользой. И regression-логика как раз помогает удержать эту композицию в здравом состоянии.
8. Сравнение уровней: что они реально доказывают
Полезно один раз увидеть все уровни рядом не как набор модных слов, а как таблицу силы и границ.
| Уровень | Что обычно реально работает | Лучше всего ловит | Чего сам по себе не доказывает | Цена |
|---|---|---|---|---|
| Unit | один класс, правило или маленький сервис | локальные бизнес-правила, преобразования, инварианты | HTTP-контракт, wiring, реальную базу, security-chain | низкая |
| Slice | один слой и его инфраструктура | JSON-контракты, web-границу и поведение конкретного слоя данных | полную цепочку приложения и соседние слои | низкая / средняя |
| Integration | несколько слоёв или полный контекст | wiring, совместную работу controller/service/repository, конфигурацию | транспортные нюансы live-server и поведение реальной внешней инфраструктуры, если она замещена | средняя / высокая |
| Live-server | реальный HTTP-сервер и клиентский вызов | end-to-end web-поведение глазами клиента | специфические особенности внешней БД или интеграции, если они не участвуют по-настоящему | высокая |
| Container | приложение плюс реальный внешний компонент | поведение PostgreSQL, миграций, инфраструктурных ограничений | всё остальное «автоматом»; это не универсальная замена всему набору тестов | высокая+ |
Эта таблица полезна тем, что убирает лишнюю магию. Например, integration-тест не «лучше unit-теста вообще». Он просто доказывает другое. А unit-тест не «слабее live-server по определению». Он просто отвечает на другой инженерный вопрос.
9. Один пользовательский сценарий не равен одному тесту 💥
Вот здесь и появляется главный момент дня. Возьмите тот же flow «editor submit → admin approve → public read». Если смотреть на него глазами пользователя, это одна история. Если смотреть на него глазами инженера качества, это набор разных рисков и, значит, набор разных доказательств.
Локальное правило перехода статуса лучше всего ловится unit-тестом. Правильный 404 на public read честнее всего проверять на web-границе. Фильтр по PUBLISHED живёт в слое данных. Сквозная связка controller → service → repository имеет смысл на integration-уровне. Реальный HTTP-обмен — на live-server. Специфика PostgreSQL — на container-уровне.
Как только это становится видно, исчезает соблазн искать один «универсальный» тип теста. И именно в этот момент вся типология перестаёт казаться бюрократией. Она начинает работать как карта цены и уверенности. То есть ровно так, как и должна работать в профессиональном backend-е 🚀
10. Типичные ошибки при разговоре про уровни тестов 💉
Ошибка №1: считать, что более широкий тест автоматически лучше.
Широкий тест действительно видит больше окружения, но платит за это скоростью и качеством диагностики. Он полезен только там, где эта ширина даёт новую информацию.
Ошибка №2: называть уровень по библиотеке, а не по реально поднятому миру.
Название файла SomethingIntegrationTest ещё ничего не доказывает. Смотрите на фактическое окружение: что было настоящим, а что нет.
Ошибка №3: ждать от unit-тестов гарантий HTTP-контракта или поведения базы.
Это не недостаток unit-тестов, а неправильное ожидание. Они хороши именно тем, что ловят локальные риски быстро и точно.
Ошибка №4: пытаться делать всё через live-server или полный контекст.
Такой suite быстро становится дорогим, шумным и неповоротливым в ежедневном использовании. Если разработчику не хочется запускать тесты часто, стратегия уже проигрывает.
Ошибка №5: воспринимать regression suite как «самый большой набор тестов».
Регрессионный набор ценен не размером, а ROI. Его задача — быстро и регулярно давать сильный сигнал на изменениях, а не впечатлять длиной лога.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ