1. Startup smoke test: что это
Startup smoke test — это тест, который выглядит почти как «ничего не делает», но на самом деле делает одну важную вещь: заставляет Spring Boot собрать ApplicationContext. Если контекст собрался, значит как минимум не развалился wiring, не упала конфигурация, не случился конфликт бинов и приложение в целом стартует в заданной тестовой конфигурации. Это похоже на “проверку зажигания” у машины: вы ещё никуда не поехали, но уже узнали, что аккумулятор жив, стартер крутит, а двигатель хотя бы не отваливается на месте.
Когда отделяешь полный контекст от реального сервера, smoke test перестаёт казаться странным. Ему не нужен порт и не нужен HTTP-сценарий; ему нужен честный ответ на вопрос: приложение вообще собирается в заданной тестовой конфигурации?
Ключевая идея — smoke test намеренно не лезет в бизнес-логику. Он не пытается проверить PublicationPolicy, не сравнивает JSON, не ходит в контроллеры. Он честно работает на уровне «система собирается». И это не “слабость”, а сила: тест маленький, понятный по смыслу и часто ловит такие поломки, которые unit- и slice-тесты даже не обязаны видеть (потому что они сознательно не поднимают половину приложения).
Для ContentHub smoke test особенно полезен как страховка от классических “интеграционных” граблей: кто-то поменял YAML, переименовал property prefix, не обновил @ConfigurationProperties, добавил новый бин с таким же именем, или включил auto-configuration, которая теперь требует настройки. Всё компилируется, локальные unit-тесты зелёные — а приложение в реальности перестало стартовать. Smoke test это поймает.
2. Что доказывает smoke test
Очень легко попасть в психологическую ловушку: раз тест называется “smoke” и зелёный — значит «всё хорошо». А потом прод падает, и вы смотрите на зелёный квадратик в отчёте тестов как на предателя. Но он не предатель, он просто делал ровно то, что обещал: проверял старт.
Давайте зафиксируем это максимально явно. Удобнее всего — в таблице, потому что мозг любит таблички (а ещё таблички не спорят с нами и не требуют кофе).
| Вопрос | Smoke test отвечает? | Почему |
|---|---|---|
| Поднимается ли ApplicationContext без ошибок wiring/конфигурации? | Да | Это буквально цель теста: пройти фазу старта контекста. |
| Создался ли ключевой бин (например, ArticleWorkflowService)? | Да (если вы это проверите) | Можно добавить 1–2 “якорные” проверки, не превращая тест в сценарий. |
| Работает ли конкретное бизнес-правило (например, переход статуса IN_REVIEW -> PUBLISHED)? | Нет | Это уже ответственность unit / integration flow, а не smoke. |
| Корректен ли JSON-контракт DTO? | Нет | Это зона @JsonTest и/или MVC-тестов, smoke тест про другое. |
| Правильно ли работает query в репозитории? | Нет | Для этого был @DataJpaTest. Smoke не обязан доказывать query behavior. |
| Правильно ли настроены HTTP-статусы и error contract? | Нет | Это web-layer зона (в узких MVC тестах или позже в wiring flow). |
| Доказано ли, что приложение реально обслуживает HTTP-запросы на порту? | Зависит от webEnvironment, но “обычный” smoke чаще нет | Если вы запускаете NONE/MOCK, сервера нет. А сегодня мы именно про минимальную проверку старта. |
Если сказать одним предложением, smoke test — это «приложение заводится», а не «приложение ездит, паркуется, проходит техосмотр и ещё делает вам кофе».
3. Выбор webEnvironment: NONE vs MOCK
На предыдущей лекции мы познакомились с режимами webEnvironment. Теперь важный практический вопрос: какой режим подходит именно для smoke test, если мы хотим проверить старт приложения как системы, но не хотим лишней цены и лишних деталей?
Чаще всего в проекте есть два разумных варианта для startup smoke tests: NONE и MOCK. Разница между ними не “косметическая”, она реально меняет тип контекста, который будет поднят. И тут полезно мыслить прагматично: чем меньше вы поднимаете, тем быстрее тест, но тем меньше “поверхности” вы проверяете. Чем больше поднимаете — тем дороже, но тем больше вероятность поймать проблему в web-настройках.
Небольшая подсказка в виде мини-таблицы:
| Режим | Что это по смыслу | Когда smoke test с ним полезен |
|---|---|---|
| NONE | “Приложение как набор бинов, без web-среды” | Когда вы хотите самый простой сигнал “контекст собрался” и не хотите тащить web-конфигурацию. |
| MOCK | “Приложение под web, но без реального сервера” | Когда вы хотите, чтобы web-часть тоже поднялась, но без запуска порта. |
В рамках ContentHub (MVC backend) NONE — очень хороший дефолт для smoke test, потому что он отвечает на минимальный вопрос и не заставляет тест быть “чуть-чуть MVC”. Но если команда когда-то наступала на грабли вида “контекст поднялся, но web-часть сломана из-за конфигурации/фильтров/компонентов MVC” — тогда smoke test на MOCK может быть оправдан. Важно только помнить, что это всё равно smoke: мы не тестируем HTTP-контракт, мы просто позволяем web-слою подняться.
4. Минимальный smoke test: contextLoads()
Базовый smoke-path обычно выглядит именно так.
Есть тест, который выглядит как шутка (особенно для новичка): пустой метод contextLoads(). У него нет assertThat(), нет моков, нет проверок. И тем не менее, он может быть одним из самых “дорогих” по тому, что проверяет: он запускает старт приложения. Если старт провалился, тест упадёт ещё до выполнения тела метода — на этапе поднятия контекста.
Для ContentHub мы обычно фиксируем smoke test как отдельный класс с максимально понятным названием. Очень важно, чтобы тест использовал test-профиль, иначе вы легко получите ситуацию “на моём ноуте тест зелёный, а в CI падает”, потому что подхватились не те настройки.
Пример минимального smoke test (обратите внимание на webEnvironment = SpringBootTest.WebEnvironment.NONE и @ActiveProfiles("test")):
import org.junit.jupiter.api.Test; // базовая аннотация теста
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE) // Поднимаем контекст без web-сервера
@ActiveProfiles("test") // Жёстко фиксируем тестовый профиль, чтобы не уехать в случайные настройки
class ContentHubApplicationSmokeTest {
@Test
void contextLoads() {
// Тело пустое: если контекст не соберётся, тест упадёт ещё до выполнения этого метода
}
}
Этот тест проверяет простую вещь: в окружении test (то есть с application-test.yml, о котором вы говорили раньше) приложение ContentHub способно собрать контекст. Он не доказывает, что статья публикуется, но он доказывает, что вы вообще можете дойти до момента, когда статья теоретически могла бы публиковаться.
Отдельный бонус для начинающего разработчика: когда такой тест падает, он обычно падает очень информативно. В stacktrace почти всегда можно найти “корень зла”: “не смог создать бин”, “не нашёл property”, “конфликт имен”, “ошибка миграции” и так далее. Да, поначалу stacktrace выглядит как “роман Толстого”, но там есть полезные главы.
5. Smoke test с проверкой ключевых бинов
Пустой contextLoads() — это нормально, но иногда хочется чуть усилить smoke test так, чтобы он проверял не только “контекст собрался”, но и “вот эти штуки точно существуют”. Главное — не сорваться и не сделать из smoke test полноразмерный интеграционный сценарий. Хороший smoke test остаётся маленьким: проверяет один-два “якоря”.
В ContentHub такими якорями часто становятся ArticleWorkflowService и ArticleRepository. Почему? Потому что они проходят через важные куски wiring: сервисный слой и data-слой. Если они инжектятся — значит, как минимум базовая связка слоёв в контексте живёт.
Пример smoke test с проверкой двух бинов:
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
import com.example.contenthub.repository.ArticleRepository;
import com.example.contenthub.service.ArticleWorkflowService;
import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE)
@ActiveProfiles("test")
class KeyBeansSmokeTest {
@Autowired
ArticleWorkflowService workflowService; // Якорный бин: проверяем, что сервисный слой реально поднялся
@Autowired
ArticleRepository articleRepository; // Якорный бин: проверяем, что data-слой тоже присутствует в контексте
@Test
void keyBeansExist() {
// Smoke-проверка: фиксируем сам факт наличия бинов, не трогаем бизнес-логику
assertThat(workflowService).isNotNull();
assertThat(articleRepository).isNotNull();
}
}
Здесь важно удержать правильную “наглость” теста. Мы не вызываем workflowService.publish(...), не создаём статью, не проверяем статус. Мы просто фиксируем, что эти бины создаются. Это всё ещё smoke test: он про сборку системы, а не про поведение.
Если вы спросите: “А зачем это, если контекст и так поднялся?” — ответ такой: иногда контекст может подняться, но конкретный бин может быть отключён условной конфигурацией (@ConditionalOnProperty и т.п.) или заменён чем-то неожиданным. Да, это уже не самый частый случай, но проверка двух “якорных” бинов иногда делает диагностику ещё проще: вы сразу видите, чего не хватает.
6. Полезные нюансы smoke tests
Локальные property overrides
Иногда smoke test нужен не только как “приложение стартует”, но и как “приложение стартует с определённой настройкой”. Например, вы подозреваете, что кто-то поменял структуру настроек вложений (contenthub.attachments.*), и теперь binding ломается. Или вы добавили новую настройку, и хотите убедиться, что она не конфликтует с application-test.yml.
У @SpringBootTest есть параметр properties, который позволяет локально переопределить значения. Это удобно, но у него есть цена: он может породить новый тестовый контекст, а значит сделать suite медленнее. Вспоминаем идею из более ранних дней про context caching: “каждый уникальный набор конфигурации — потенциально отдельный контекст”.
Пример smoke test, который проверяет старт с локальным override:
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
@SpringBootTest(
webEnvironment = SpringBootTest.WebEnvironment.NONE,
properties = "contenthub.attachments.max-size=1048576" // Локально переопределяем свойство, чтобы проверить binding/валидацию
)
@ActiveProfiles("test")
class SmokeWithPropertyOverrideTest {
@Test
void contextLoadsWithOverride() {
// Важно: мы не проверяем поведение, только факт успешного старта с этим набором настроек
}
}
Этот тест по смыслу проверяет: “с такой настройкой контекст тоже собирается”. Никакой бизнес-логики. Никаких запросов. Никакой БД-магии. Просто старт.
Когда такой тест оправдан? Тогда, когда у вас реально был риск “из-за этой настройки приложение упадёт при старте” или “binding/validation конфигурации сломается”. Когда не оправдан? Когда вы начинаете делать десять smoke tests с десятью разными значениями — и suite внезапно становится медленным, потому что контекстов стало слишком много. Smoke test должен экономить жизнь, а не отнимать её.
Сколько smoke tests держать в проекте
Есть обидная закономерность: как только команда узнала про smoke tests, появляется соблазн наделать их “на всё”: отдельный smoke test для storage, отдельный для moderation, отдельный для security, отдельный для каждого профиля, отдельный для каждого… и в какой-то момент это перестаёт быть “smoke” и становится “костёр”.
В нормальном живом проекте чаще всего достаточно очень маленького набора: один базовый ContentHubApplicationSmokeTest и, максимум, ещё один-два, которые проверяют особые ветки конфигурации. В учебном ContentHub вообще хорошо держаться минимализма: один “якорный” smoke test и один “с override” (если вам нужно зафиксировать настройку).
Почему много smoke tests — это шум? Потому что они почти все проверяют одно и то же: старт контекста. Если контекст стартует, второй тест “контекст стартует ещё раз” не добавляет новую уверенность, а только добавляет время выполнения. Гораздо полезнее вложить энергию в более точные тесты слоёв: unit, slices, data, и уже потом — несколько продуманных full wiring tests.
Есть ещё один практический момент: smoke tests особенно чувствительны к “случайным” изменениям тестовой конфигурации. Если вы где-то включили лишний импорт, добавили лишний @TestConfiguration, начали подключать компоненты, которые не нужны — smoke test начинает падать “из ниоткуда”. Поэтому хороший smoke test, как хороший тостер, должен быть простым. Нажал кнопку — он работает. Не надо прошивать в тостер операционную систему.
7. Чтение падения smoke test
Когда smoke test падает, у новичка часто два состояния: “я сломал вселенную” и “Spring меня ненавидит”. На самом деле Spring к вам нейтрален, просто он многословен. И smoke test как раз хорош тем, что падение обычно происходит рано, а значит причина часто близко к поверхности.
Практическая техника чтения падения выглядит так. Сначала вы находите в stacktrace место, где написано что-то вроде Failed to load ApplicationContext. Это почти всегда верхний симптом. Дальше вы ищете Caused by: и идёте вниз, пока не увидите более предметный кусок: например, что не создался определённый bean или не забиндились свойства.
Типовые причины падения smoke tests в Spring Boot проектах (включая ContentHub) обычно такие: неправильные настройки datasource (тестовый профиль не активировался и приложение пытается подключиться к “боевой” базе), ошибка миграции Flyway (скрипт невалидный или конфликт версий), конфликт бинов (два бина одного типа/имени, и Spring не знает, что инжектить), проблема с @ConfigurationProperties (изменили префикс или структуру YAML, а класс остался прежним), и, иногда, ошибки валидации настроек (если на properties-классе есть Bean Validation ограничения).
Smoke test не обязан учить вас жить без stacktrace, но он помогает сделать stacktrace “смысловым”: вы точно знаете, что проблема в старте контекста. Это уже сильно сужает поиск. И да, бывает, что причина — банальная опечатка в YAML. Очень “дорогая” опечатка, но банальная.
8. Типичные ошибки при написании startup smoke tests
Ошибка №1: считать smoke test “главным тестом”, который заменяет всё остальное.
Часто после появления первого зелёного contextLoads() возникает иллюзия “ну значит приложение работает”. На самом деле smoke test доказывает только старт контекста. Бизнес-правила, JSON-контракты, query-поведение репозиториев и HTTP-статусы живут в других слоях тестирования. Smoke test — это входная дверь, но не вся квартира.
Ошибка №2: не фиксировать тестовое окружение и случайно запускаться не в test-профиле.
Если не указать @ActiveProfiles("test") (или эквивалентный способ включить test-настройки), тест может внезапно подхватить не те значения: например, попытаться подключиться к реальной базе, взять production URL moderation-сервиса или использовать не тот storage path. Итог — плавающие падения “вроде бы ничего не меняли”. Smoke test любит детерминизм: один профиль, понятные YAML, никаких сюрпризов.
Ошибка №3: превращать smoke test в сквозной сценарий “на полприложения”.
Иногда хочется прямо в smoke test создать статью, отправить на ревью, опубликовать и ещё проверить публичную выдачу. Это уже не smoke test, это интеграционный flow-тест, и он имеет право быть, но в другом месте и с другим смыслом. Smoke test должен оставаться минимальным: контекст поднялся, максимум — ключевые бины на месте.
Ошибка №4: делать десятки smoke tests с разными properties overrides и случайно убить скорость suite.
Локальные overrides полезны, но каждый уникальный набор свойств может породить новый контекст, а значит увеличить время выполнения всей пачки тестов. Если вы начали “щупать настройки” и на каждый вариант сделали новый @SpringBootTest(properties = ...), вы почти гарантированно получите медленный набор тестов с низким ROI. Smoke tests должны быть редкими и стратегическими.
Ошибка №5: проверять в smoke test поведение вместо факта сборки (и спорить с собой о причинах падений).
Если вы начинаете в smoke test вызывать методы сервисов и проверять результат, вы попадаете в странную зону: тест уже не “минимальная проверка старта”, но ещё и не полноценный wiring test с чётким сценарием. В такой полусередине падение теста становится труднее интерпретировать: то ли контекст не поднялся, то ли поведение не то, то ли данные не подготовили. Smoke test хорош именно ясностью вопроса; не стоит его размывать.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ