1. Зелений startup — лише smoke-сигнал 🌋
У світі Spring Boot фраза «застосунок стартує» зазвичай означає таке: JVM запустила main, Spring зібрав ApplicationContext, створив біни, прочитав конфігурацію і, можливо, підняв вбудований сервер. Це корисна перевірка. Такий старт справді допомагає виявити реальні проблеми: конфліктні біни, биту конфігурацію, несумісні залежності, іноді — неробочий datasource або помилку в @Configuration. Тобто сам запуск не марний. Він просто відповідає на дуже вузьке запитання: «система взагалі може ввімкнутися?»
@SpringBootApplication // Точка входу Spring Boot: активує автоконфігурацію і сканування компонентів
class ContentHubApplication {
public static void main(String[] args) {
// Запуск застосунку: підіймається ApplicationContext і починається життєвий цикл Spring
SpringApplication.run(ContentHubApplication.class, args);
}
}
Якщо цей код виконався і застосунок не впав під час старту, це добрий знак. Але добрий знак — ще не доказ коректної поведінки. Старт не знає, який код статусу має повернути публічна кінцева точка, коли статтю не знайдено. Старт не знає, чи можна публікувати DRAFT в обхід рев’ю. Старт не знає, чи повинен анонімний користувач бачити лише PUBLISHED, а не все підряд. І вже точно старт не перевіряє, що зовнішній moderation-сервіс відповість саме так, як ви від нього очікуєте.
Це важлива різниця, бо в початківця backend-розробника дуже легко виникає хибний зв’язок: «раз сервер піднявся, отже все в порядку». Насправді в цей момент сервер перевірив лише власну здатність запуститися. Клієнт спілкується не з вашим ApplicationContext, а з API, бізнес-правилами і даними.
2. Один реальний сценарій
Візьмімо один реальний сценарій, який проходитиме через увесь рівень. Редактор створює чернетку статті, надсилає її на рев’ю, адміністратор підтверджує публікацію, а анонімний користувач читає статтю за slug. Для людини це одна історія. Для backend-а — одразу кілька меж: HTTP, бізнес-правила, дані, доступ, зовнішні виклики і, часом, асинхронні побічні ефекти.
Поки ви дивитеся на цей процес очима «ну воно ж запускається», усе здається простим. Але щойно ви запитуєте «що саме тут може зламатися?», історія швидко перестає бути нудною. Публічна видача може показати неопубліковану статтю. Редактор може надіслати на рев’ю не свою статтю. Адмін може випадково отримати можливість публікувати запис із неправильного статусу. Валідація може пропустити сміттєвий запит. Модерація може повернути неочікувану відповідь. І кожну з цих поломок startup пропустить повз себе.
Ручна перевірка успішного сценарію тут допомагає не набагато більше. Ви смикнули одну-дві кінцеві точки, побачили 200 OK і заспокоїлися. Але успішний сценарій майже завжди перевіряє саме те, що ви й так очікували побачити. Він погано ловить заборони, хибні негативні гілки, помилки вибірок і несумісні контракти. Тобто ручний smoke корисний як швидкий огляд, але не як стратегія якості.
3. HTTP і JSON ламаються тихіше, ніж здається
Перша велика зона ризику — API-границя. Клієнт спілкується не з вашим ApplicationContext, а з URL, статусами, заголовками і JSON-полями. Тому багато болючих дефектів живуть саме тут: не той шлях, не той код статусу, не той формат помилки, зайве поле, зникле поле, забута валідація. І все це цілком може існувати в застосунку, який чудово стартує.
@RestController // HTTP-шар: сюди приходять запити клієнтів
class PublicArticleController {
private final ArticleRepository repository; // Залежність на шар даних (репозиторій)
@GetMapping("/api/public/articles/{slug}") // Маршрут: публічна кінцева точка за slug
ArticleResponse getBySlug(@PathVariable String slug) { // slug береться зі шляху запиту
// Шукаємо статтю в базі за slug; тут поки що немає фільтра за статусом (PUBLISHED тощо)
Article article = repository.findBySlug(slug).orElseThrow(); // orElseThrow без мапування часто перетворюється на 500
// Формуємо DTO відповіді: те, що побачить клієнт (контракт API)
return new ArticleResponse(article.getTitle(), article.getBody());
}
}
На вигляд це нормальний контролер. Але в нього одразу кілька потенційних проблем. По-перше, findBySlug взагалі не гарантує, що статтю опубліковано. Отже, public endpoint може повернути чернетку. По-друге, orElseThrow() без осмисленого мапування часто завершується 500, хоча клієнту потрібен 404 Не знайдено. По-третє, сам ArticleResponse може тихо змінитися: ви перейменуєте поле, зміните формат дати або забудете обов’язкову частину payload, а застосунок усе одно буде щасливо стартувати 🚨
Сюди ж належить і банально забута валідація. Ви прибрали @Valid або послабили constraint на request DTO, і API почав приймати те, чого не повинен. Контекст від цього не розвалиться. Трохи пізніше розваляться доменна логіка, база даних або чийсь клієнт. Саме тому «endpoint відповів» і «endpoint відповів правильно» — це зовсім не одне й те саме.
4. Бізнес-правила ніколи не падають красиво
Бізнес-логіка ламається особливо підступно: код компілюється, сервер працює, винятків немає, а продукт уже поводиться неправильно. Для ContentHub це особливо помітно на процесі переходів статусів статті. Публікація має відбуватися з зрозумілого стану, зазвичай після рев’ю. Якщо правило переходу розмазане по коду або просто забуте, backend починає дозволяти заборонене.
class ArticleWorkflowService { // Сервіс доменної логіки: керує переходами статусів
void approve(Article article) {
// Тут відбувається ключова бізнес-дія: "схвалити" статтю
// Важливо: у такому вигляді немає перевірки поточного статусу (наприклад, що він IN_REVIEW)
article.setStatus(ArticleStatus.PUBLISHED); // Прямий перехід у PUBLISHED — потенційна поломка workflow
}
}
Технічно тут усе «працює». Але доменна ціна такої стрічки величезна. Вона дає змогу перевести статтю в PUBLISHED без перевірки, чи була вона взагалі в IN_REVIEW. А це вже не дрібна неточність. Це поломка бізнес-процесу, через яку публічна видача, сповіщення та аудиторські сліди починають жити в неправильній реальності.
І тут важливий дорослий момент. Spring уміє дуже багато, але не знає сенсу вашого процесу. Він не буде захищати правило «publish only after review» лише тому, що ви красиво написали сервіс. Сенс домену завжди залишається вашою відповідальністю, а отже — і вашою зоною тестування.
5. Дані вміють брехати
У початківця backend-розробника шар даних часто виглядає майже нейтрально: є репозиторій, є save, є find — що тут може піти не так? На практиці саме шар даних дає багато тихих регресій. Неправильна вибірка, забутий фільтр, не той порядок сортування, дірявий constraint — і користувач бачить зовсім не те, що ви йому обіцяли на рівні API.
interface ArticleRepository { // Контракт репозиторію: описує доступ до даних (наприклад, через Spring Data)
// Метод вибірки за категорією; важливо: у назві немає фільтра за статусом публікації
List<Article> findAllByCategoryCode(String code);
}
Якщо таким методом користуватися для публічного каталогу, дуже легко забути фільтр за PUBLISHED. З погляду Spring Data все добре: метод валідний, застосунок стартує, запит виконується. З погляду продукту — катастрофа, бо public-розділ раптом починає показувати статті DRAFT, IN_REVIEW або REJECTED.
Те саме стосується обмежень даних. slug має бути унікальним, деякі поля — не null, деякі зв’язки — обов’язковими. Якщо в цій частині моделі є діри, баг не обов’язково проявиться в момент старту. Він спливе, коли застосунок зустріне конкретні дані. А це вже найнеприємніший тип дефекту: він з’являється не там, де ви його чекаєте.
6. Доступ не можна відкладати «на потім» 🔒
Є ще одна зона, яку особливо люблять недооцінювати: доступ. Здається, що спершу можна «зробити функціонал», а безпеку й обмеження прикрутити потім. Майже завжди це завершується тим, що у вас уже є робочий API, але працює він не для тих людей і не на тих умовах, для яких має працювати.
boolean canEdit(Article article, String username) {
// Перевірка "чи є користувач власником": редагувати може лише автор статті
return article.getAuthorUsername().equals(username)
// Перевірка за workflow: редагування дозволено лише у статусі DRAFT
&& article.getStatus() == ArticleStatus.DRAFT;
}
Навіть у такому короткому правилі живуть два різні ризики. Перший — owner-based access: редагувати може лише автор. Другий — workflow restriction: редагування дозволено лише в певному статусі. Якщо втратити хоча б одну половину умови, ви отримаєте або security-дірку, або поломку доменної логіки.
Те саме стосується ролей. Анонімний користувач не має бачити editor/admin-дії, editor не має змінювати чужу статтю, admin не має випадково отримувати обхід доменних обмежень лише тому, що він admin. Усе це не перевіряється фактом старту. Це перевіряється лише поведінкою. І дуже корисно вже в перший день побачити, що «хто може» і «за якого статусу можна» — це не одна, а дві різні перевірки 🔐
7. Зовнішні межі живуть за своїми правилами
Проєкт ContentHub не закінчується на контролерах і базі. У нього є зовнішня модерація, файлове сховище, сповіщення про публікацію та інші технічні межі. Вони особливо підступні, бо можуть бути повністю зламані й усе одно не заважати застосунку стартувати. Поки ви не викликали конкретний адаптер, Spring чесно вважає, що все гаразд.
interface ModerationClient { // Клієнт зовнішнього сервісу: межа, що має власний контракт і власні збої
// Перевірка контенту на блокування; на практиці тут можуть бути таймаути/помилки/неочікувані відповіді
boolean isBlocked(String title, String body);
}
Проблема тут уже не в одному рядку коду, а в поведінці межі. Зовнішній сервіс може таймаутити, змінювати контракт, повертати неочікуваний статус або просто бути недоступним. Файлове сховище може зламатися через права доступу або неправильний шлях. Сповіщення може не піти, хоча основна дія, здається, «успішно завершилася».
Це важливе протверезіння для backend-розробника. Наявність інтерфейсу або красивого адаптера не робить межу надійною. Вона лише робить її зручнішою для проєктування і тестування. Надійність з’являється тільки там, де ви реально перевіряєте поведінку цієї межі на потрібній глибині.
8. Як перевіряти по-дорослому
Коли ви складаєте всі ці зони разом, видно просте правило: тестування потрібне не для того, щоб «прикрасити проєкт тестами», а для того, щоб зробити різні класи поломок спостережуваними. У цьому сенсі startup-check — один інструмент. Ручний успішний сценарій — другий. Але повноцінна стратегія тестування починається лише там, де ви перестаєте змішувати їх в одну велику ілюзію впевненості.
| Що ви перевірили | Що це насправді доводить | Що залишається в тіні |
|---|---|---|
| Застосунок стартує | Контекст зібрався, базове зʼєднання працює | Контракти API, бізнес-правила, доступ, коректність даних, інтеграції |
| Руками смикнули 1–2 успішні сценарії | Кілька обраних гілок зараз відповідають | Негативні випадки, регресії, рідкісні поєднання даних, відтворюваність |
| Є продумана стратегія тестів | Різні ризики стають видимими на своїй глибині | Лише те, що ви свідомо вирішили поки не покривати |
Тут уже видно дорослу межу курсу. Ми не робитимемо вигляд, ніби один великий smoke-тест замінює все. І не впадатимемо в іншу крайність — писати перевірки заради красивої статистики. Нам потрібна система, яка дає зрозумілий, повторюваний і економний зворотний зв’язок.
Щойно це стає зрозуміло, наступне запитання виникає саме собою. Якщо поломки живуть у різних місцях, backend потрібно спершу розкласти за зонами ризику. Інакше вибір будь-якого тесту буде випадковим. Саме до цієї карти ми зараз і перейдемо 🗺️
9. Типові помилки під час першого погляду на якість 🚧
Помилка № 1: вважати, що зелений startup дорівнює якості застосунку.
Startup корисний, але він перевіряє лише факт складання системи в базовому стані. Він не доводить коректність поведінки користувача, не захищає від витоків даних і не знає нічого про ваші бізнес-правила.
Помилка № 2: плутати «endpoint відповів» і «endpoint відповів правильно».
200 OK сам по собі нічого не гарантує. Потрібні правильні дані, правильний статус, коректний контракт помилки і правильні обмеження доступу. Інакше у вас просто гарна, але неправильна відповідь.
Помилка № 3: довіряти ручному успішному сценарію як основному захисту від регресій.
Ручна перевірка добра як швидкий огляд, але вона не масштабується і не відтворюється з машинною точністю. Те, що ви не здогадалися перевірити сьогодні, спокійно повернеться багом завтра.
Помилка № 4: відкладати проблеми безпеки й даних «на потім».
Зазвичай саме таке «потім» і стає найдорожчим. Дані та доступ — не додаткові прикраси до API, а частина його коректності.
Помилка № 5: бачити backend як один чорний ящик.
Щойно все зводиться до «працює / не працює», ви втрачаєте інженерну точність. Корисне тестування починається в той момент, коли ви вмієте назвати місце ризику, а не просто констатувати, що щось десь зламалося.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ