JavaRush /Курси /Spring Test /Де ховаються реальні збої 🧠

Де ховаються реальні збої 🧠

Spring Test
Рівень 1 , Лекція 1
Відкрита

1. Починати варто з дій користувача 💉

Дуже спокусливо будувати карту ризиків «за підручником»: controller, service, repository, а далі якось розберемося. Це краще, ніж нічого, але надто сухо. Користувач не живе всередині ваших пакетів. Він виконує дії: читає статтю, створює чернетку, надсилає текст на ревʼю, намагається змінити вкладення, отримує відмову в доступі. Якщо починати не з дій, а відразу з папок, легко втратити сенс і почати тестувати архітектурні ярлики замість реальної поведінки.

Для проєкту ContentHub це особливо важливо через три різні контури доступу. У нас є public-контур для читання опублікованих статей, editor-контур для авторської роботи з матеріалами й admin-контур для модерації та публікації. Одна й та сама доменна дія може звучати однаково, але ризик у ній буде різним залежно від зони.

«Переглянути статтю» для public означає фільтр за PUBLISHED. «Переглянути статтю» для editor може означати доступ до власної чернетки. «Переглянути статтю» для admin може включати службові поля та розширений контекст. Якщо не бачити цієї різниці, карта ризиків одразу стає плоскою.

2. Один сценарій уже показує, скільки тут дірок

Давайте візьмемо типовий сценарій без деталей реалізації. Спочатку editor створює DRAFT. Потім надсилає статтю на ревʼю. Далі admin або схвалює її, або відхиляє. Після схвалення стаття стає видимою в public API. На папері це майже лінійний бізнес-процес. Усередині бекенду — цілий ланцюг перевірок, вибірок, мапінгу даних, обмежень доступу й зовнішніх викликів.

Уже на одному цьому маршруті видно кілька різних класів багів. Editor може надіслати на ревʼю статтю не з того статусу. Admin може опублікувати матеріал в обхід модерації. Репозиторій може повернути запис без фільтра за статусом. Public endpoint може віддати 500 там, де клієнт очікує 404. Security може переплутати ролі, а перевірка ownership — власника ресурсу. Зовнішня модерація може таймаутити або повернути дивний вердикт. Усе це різні поломки, хоча зовні вони вбудовані в один і той самий продуктовий сценарій.

І тут уже видно, чому один великий тест «на всю історію» часто недостатньо точний. Він може показати, що щось пішло не так, але не пояснить, де живе ризик і який шар справді дав збій. А карта ризиків якраз учить спочатку локалізувати поломку, а вже потім обирати глибину перевірки.

3. Сутність несе в собі одразу кілька осей ризику

Корисно один раз подивитися на доменну сутність не як на «просто entity», а як на носій ризиків. У статті в ContentHub є поля, які беруть участь одразу в кількох правилах: slug, автор, статус, категорія, контент, дата публікації. Одне поле — і кілька потенційних джерел поломок.

class Article {

    // Бере участь у HTTP-контракті (URL/параметр) і в правилах унікальності в базі/домені
    private String slug;

    // Потрібне для перевірки ownership: хто саме володіє статтею
    private String authorUsername;

    // Ключове поле workflow: з якого стану в який можна перейти
    private ArticleStatus status;

    // Впливає на фільтрацію/пошук/видачу (і часто на доступність у різних контурах)
    private String categoryCode;
}

slug бере участь у HTTP-контракті й у вимогах до унікальності. authorUsername потрібен для owner-based access. status визначає і workflow, і публічну видимість. categoryCode впливає на фільтрацію та пошукову видачу. Тобто стаття — це не «одна сутність, один ризик». Це вузол, де перетинаються правила видимості, доступу, даних і публічної поведінки.

Саме тому карта ризиків корисніша за звичайний перелік класів. Вона змушує дивитися не на назву пакета, а на те, який тип дефекту може народитися навколо конкретного шматка домену. В одному місці зламається JSON, в іншому — вибірка, у третьому — обмеження доступу, хоча обʼєкт у вас усе той самий.

4. Controller-ризик — це ризик контракту

На шарі контролерів найчастіше ламається не сама бізнес-ідея, а договір із зовнішнім світом. Шлях, метод, параметри, status code, форма успішної відповіді, форма помилки, правила валідації — усе це живе на HTTP-межі. Тому controller-ризик краще формулювати не як «зламався контролер», а як «клієнту віддали не той контракт».

Типовий приклад для public API: endpoint за slug знайшов статтю, але не перевірив, що вона опублікована. Або не знайшов статтю й повернув 500, тому що необроблений виняток вилетів угору. Або прийняв request без обовʼязкового поля, бо забули валідацію. В усіх трьох випадках застосунок може ідеально стартувати. Поломка стане видимою лише тоді, коли зовнішній клієнт упреться саме в цю межу.

Важливо памʼятати ще й про інше. Controller часто показує дефект не того шару, який його породив. Public endpoint може повернути чернетку не тому, що сам контролер «поганий», а тому, що сервіс або репозиторій віддали йому невідповідні дані. Тому карта ризиків не шукає винного. Вона допомагає зрозуміти, де саме треба зробити поведінку спостережуваною, щоб потім не стріляти навмання.

5. Service-ризик — це суть застосунку

Service-шар — це місце, де ваш бекенд перестає бути просто web-обгорткою над базою і починає поводитися як продукт. Тут живуть правила: з якого статусу можна перейти в який, хто може редагувати матеріал, коли потрібно звертатися до зовнішньої модерації, що робити з її результатом, коли стаття стає публічною. Саме тому service-ризик зазвичай повʼязаний із порушенням інваріантів, а не з формою JSON.

class ArticleWorkflowService {

    void submitForReview(Article article) {
        // Критичне бізнес-правило: на ревʼю можна надсилати лише чернетку
        if (article.getStatus() != ArticleStatus.DRAFT) {
            // Помилка саме доменного інваріанта, а не «технічна проблема контролера»
            throw new IllegalStateException("INVALID_STATUS");
        }

        // Зміна статусу — точка ризику: далі вся поведінка залежатиме від цього значення
        article.setStatus(ArticleStatus.IN_REVIEW);
    }
}

Навіть у такому короткому коді одразу видно, що саме є предметом ризику. Не маршрут URL, не спосіб серіалізації і не SQL-діалект бази. Ризик у тому, що правило переходу можуть забути, послабити або випадково змінити під час рефакторингу. І тоді бекенд робитиме «технічно допустиму», але продуктово хибну дію.

Сервіси часто ще й склеюють кілька шарів одразу. Наприклад, вони читають статтю з репозиторію, перевіряють ownership, викликають moderation client, оновлюють статус і зберігають результат. Тому одна й та сама сервісна операція може стояти на стику бізнес-ризику, data-ризику, security-ризику й integration-ризику. Це не робить карту складнішою заради складності. Це просто чесний опис того, як реально влаштований звичайний бекенд-проєкт.

6. Data-ризик — це не лише про збереження

Коли мова заходить про шар даних, новачки часто бачать лише CRUD. Але більша частина дорогих багів тут узагалі не про save(). Вони про вибірки, сортування, обмеження й відповідність моделі реальній схемі. Public API може показувати зайві записи не тому, що контролер поганий, а тому, що запит повернув надто широкий набір даних. Editor може не побачити свою статтю через дивний WHERE. Два однакові slug можуть зʼявитися через діру в обмеженні унікальності. І все це — ризики шару даних.

interface ArticleRepository {

    // Небезпечний/універсальний метод: легко забути додати фільтрацію за статусом на рівні запиту
    Optional<Article> findBySlug(String slug);

    // Більш «чесний» контракт для public-видимості: статус фіксується в самому запиті
    Optional<Article> findBySlugAndStatus(String slug, ArticleStatus status);
}

Навіть різниця між цими двома методами вже показує, де саме є ризик. Якщо public-видимість має гарантуватися на рівні запиту, другий контракт чесніший і безпечніший. Якщо ж ви використовуєте перший, а фільтрацію сподіваєтеся не забути в сервісі, ризик мігрує в інший шар. Карта ризиків допомагає побачити цю міграцію заздалегідь, а не після баг-репорту 🚨

Шар даних ще й підкидає проблеми лише на певному наборі записів. Поки база порожня, усе виглядає спокійно. Щойно зʼявляються реальні комбінації статусів, авторів, дат і звʼязків, починають спливати поломки, яких не було видно на «чистому» старті.

7. Security-ризик проходить крізь кілька шарів 🔒

У безпеки є неприємна особливість: вона рідко вкладається в один пакет і дуже часто маскується під «звичайний бізнес-баг». Editor не може відредагувати статтю — це може бути коректна заборона за статусом, а може бути помилкова заборона за ownership. Анонімний користувач отримав доступ до admin-дії — це вже провал role-based access. Симптоми схожі на звичайні поломки поведінки, а ціна набагато вища.

Для ContentHub зручно одразу розділяти дві гілки security-ризику. Перша — role-based access: хто взагалі має право заходити в public, editor і admin-зони. Друга — owner-based access: чи може конкретний editor працювати саме з цією статтею. Вони не замінюють одна одну. Можна ідеально розділити ролі й усе одно забути перевірити власника ресурсу. Можна чесно перевірити власника й при цьому випадково відкрити службовий endpoint анонімному користувачу.

Ось чому security-карта починається не зі слів Spring Security. Вона починається зі слів «хто може» і продовжується питанням «за яких умов». Це важливий зсув оптики. І він потім дуже допомагає обирати тести без хаосу.

8. Integration-ризик — на межі із зовнішнім світом

Щойно в системі зʼявляється зовнішня модерація, файлове сховище, сповіщення або будь-який інший технічний адаптер, карта ризиків отримує ще одну вісь. Тут небезпека не в тому, що код «не компілюється». Небезпека в тому, що зовнішній світ поводиться не так, як вам зручно: відповідає повільно, відповідає неочікуваним форматом, взагалі не відповідає, повертає інший статус або тимчасово недоступний.

Для ContentHub moderation client — хороший приклад такої межі. Поки стаття не йде на ревʼю, ви можете навіть не помітити, що шлях виклику написаний неправильно або формат відповіді більше не збігається з вашим очікуванням. Те саме з файловим сховищем: шлях здається нормальним рівно до першого upload. І з повідомленнями: поки не дійшли до публікації, ніхто не побачить, що побічний ефект зник десь у дорозі.

Інтеграційний ризик важливо виділяти окремо ще й тому, що його легко випадково «замести під килим». Інтерфейс і акуратний адаптер створюють відчуття охайності, але не створюють гарантій коректної поведінки. Карта ризиків спеціально повертає вас до реальності: зовнішня межа — це самостійне джерело поломок, а не технічна деталь унизу стека.

9. Уся карта на одному сценарії

Якщо зібрати все разом, ContentHub перестає виглядати як абстрактний набір шарів і починає читатися як шлях проходження запиту та доменної дії. Саме тут карта стає по-справжньому корисною: ви починаєте бачити не лише «де лежать класи», а й «де живе шанс поломки».

flowchart LR
    %% Вхідна точка: будь-який зовнішній клієнт (браузер, мобільний застосунок, Postman)
    Client["Клієнт / frontend / Postman"] --> Controller["Контролер: HTTP-контракт"]

    %% Контролер передає керування в доменну або прикладну логіку
    Controller --> Service["Service: workflow і правила"]

    %% Сервіс ходить у шар даних за читанням і записом
    Service --> Repository["Репозиторій: запити та збереження"]
    Repository --> DB[(База даних)]

    %% Безпека «прошиває» кілька шарів: перевірки можуть бути і на межі, і всередині сервісу
    Security["Безпека: правила ролей і власника"] -.-> Controller
    Security -.-> Service

    %% Зовнішні межі: тут живуть timeouts, падіння контрактів і недоступність
    Service --> Moderation["Сервіс модерації"]
    Service --> Storage["Сховище файлів"]
    Service --> Notification["Надсилання сповіщень"]

А тепер той самий погляд у більш практичній формі — на одному сценарії «submit → approve → public read».

Крок сценарію Де найчастіше живе ризик Що саме ламається Як це виглядає зовні
POST /submit controller + service неправильний статус, пропущена валідація, заборона не спрацювала клієнт отримує не той status code або в системі дозволяється недопустима дія
перевірка власника service + security немає ownership-check editor змінює чужу статтю
зміна статусу на IN_REVIEW service зламаний workflow стаття йде неправильною гілкою процесу
виклик модерації integration таймаут, неправильний контракт, дивний вердикт submit зависає або переводить статтю не туди
схвалення адміністратором service + security дозволили публікацію з неправильного статусу або не тому користувачу порушено процес публікації
public read за slug repository + controller немає фільтра за PUBLISHED, неправильний 404/500, зламаний JSON анонім бачить зайве або отримує зламаний контракт

Щойно така таблиця опиняється перед очима, бекенд перестає бути «чорним ящиком». А це і є мета карти ризиків. Вона не замінює тести, але робить вибір тесту осмисленим. Без неї рівні перевірок виглядатимуть випадковим набором технік. З нею — як нормальна інженерна система.

10. Типові помилки під час побудови карти ризиків 🚧

Помилка № 1: будувати карту лише за пакетами, а не за діями користувача.
Так ви швидко отримаєте охайну, але марну схему. Корисна карта починається з product flow і лише потім лягає на controller, service, repository і зовнішні межі.

Помилка № 2: складати все в один кошик «service-логіка».
Частина багів справді живе в сервісах, але HTTP-контракт живе на web-межі, а вибірки й обмеження — у шарі даних. Чим раніше ви перестанете називати все підряд «сервісною проблемою», тим точнішими будуть ваші майбутні тести.

Помилка № 3: не розділяти role-based і owner-based доступ.
Вони звучать схоже, але ламаються по-різному. Один дефект відкриває endpoint не тій ролі, інший — не тому власнику ресурсу. Це різні ризики, і їх не можна склеювати.

Помилка № 4: вважати зовнішні інтеграції «просто технічною деталлю».
Саме на технічних межах дуже люблять жити неприємні збої, дуже схожі на production. Інтерфейс робить код чистішим, але не прибирає самого джерела ризику.

Помилка № 5: чекати, що карта ризику одразу скаже, який саме тест писати.
Карта робить крок раніше: вона локалізує природу поломки. Вибір глибини перевірки зʼявиться одразу після цього, але лише якщо місце ризику вже названо чесно.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ