JavaRush /Курси /Spring Test /Regression suite: ROI без дублів

Regression suite: ROI без дублів

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

1. Професійна регресія — компактна

Після слова “integration” у багатьох студентів вмикається режим: “тобто зараз буде найголовніший тест, який перевірить буквально все”. Це дуже людське бажання: один великий тест здається надійнішим за десять маленьких. Але в реальності regression suite — це не «найголовніший тест», а обмежений бюджет дорогих перевірок, який має окуповуватися щодня і не перетворювати запуск тестів на медитацію на progress bar.

Скажімо чесно. Дорогий наскрізний тест — як поїздка на техогляд. Він корисний, але якщо ви робите його після кожної поїздки до магазину по хліб, то не станете “найбезпечнішим водієм”. Ви просто перестанете їздити. У backend-команді відбувається те саме: надто важка регресія призводить до того, що її запускають “колись потім”, потім — “перед релізом”, а потім — “після інциденту”. І це вже не тестова стратегія, а тестова археологія.

Щоб не скотитися в крайнощі, зручно тримати в голові дуже практичну формулу:

Параметр Питання, яке ви собі ставите Що це означає для regression suite
Сигнал «Якщо цей тест упав — я одразу розумію, що зламалося і чому це важливо?» Чим більший сигнал, тим цінніший тест.
Ціна «Скільки часу він запускається і скільки зусиль потребує підтримка?» Чим більша ціна, тим рідше ви захочете його запускати.
ROI «Signal / Price» (у побутовому сенсі) Високий ROI = кандидат у регресію.

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

2. Критерії High‑ROI сценарію

Слово “важливий” звучить красиво, але мало допомагає, доки ви не навчитеся перетворювати його на конкретні критерії. High‑ROI regression‑тест — це не той, який написаний з великою кількістю анотацій і виглядає серйозно. Це той, який ловить дорогі й неприємні поломки: такі, що в unit‑тестах їх не видно, а в slice‑тестах вони виглядають як «усе ок, тільки в проді чомусь 500».

Перший критерій — критичність для користувача або домену. Якщо поломка сценарію одразу помітна користувачу (наприклад, не можна відправити статтю на review або опублікована стаття не відображається в public API), це хороший кандидат. Якщо поломка з серії «у відповіді поле updatedAt стало на секунду раніше» — це може бути важливо, але як regression‑ядро зазвичай не працює: шум вищий за користь.

Другий критерій — міжшаровість. Сценарій має реально проходити через кілька шарів: контролери, серіалізацію, валідацію, сервісну логіку, репозиторії, міграції або схему, обробку помилок. Якщо тест перевіряє лише один шар, він майже завжди може бути дешевшим (unit/slice/data). Регресія цінна саме тим, що вона підтверджує «ланцюжок», а не одну цеглину.

Третій критерій — унікальність доказу. Це мій улюблений пункт, тому що він рятує від тестової графоманії. Якщо у вас уже є якісний @WebMvcTest, який доводить контракт endpoint-а, то regression‑тест не має повторювати весь JSON до останньої коми. Його унікальна цінність — довести, що endpoint не лише повертає JSON, а й переводить дані в коректний стан і забезпечує правильні бізнес-ефекти.

Можна уявити відбір regression‑кандидатів як дуже просту схему:

flowchart TD
    A["Сценарій / ризик"] --> B{"Критично для користувача/домену?"}
    B -->|ні| X["Скоріше не в ядрі регресії"]
    B -->|так| C{"Потрібно довести ланцюжок шарів?"}
    C -->|ні| Y["Ймовірно, краще unit/slice/data"]
    C -->|так| D{"Є унікальна цінність порівняно з дешевими тестами?"}
    D -->|ні| Z["Дубль — прибираємо з регресії"]
    D -->|так| E["Кандидат у регресію з високим ROI"]

Це не догма, а фільтр від зайвого. Він допомагає не переплутати «важливо» з «цікаво».

3. Антидублювання з unit/slice/data

Дуже типова пастка: ви написали красивий наскрізний тест, і він такий великий, що рука сама тягнеться: «ну раз уже підняли контекст, давайте перевіримо ще й…». Ще пару JsonPath. Ще порівняння error payload. Ще перевіримо валідацію. Ще перевіримо pagination. Підсумок: тест стає величезним, а падіння — неочевидним. І найприкріше: половина цих перевірок уже є в інших шарах.

Щоб не дублювати, корисно буквально проговорювати: який шар уже довів цю частину поведінки. У ContentHub у нас це виглядає так.

Unit‑тести вже довели повну матрицю переходів статусів (наприклад, що не можна архівувати DRAFT). Отже, regression‑тесту не потрібно доводити «всі переходи в усіх комбінаціях» — він залишає один репрезентативний кейс, але перевіряє інше: як недопустимий перехід перетворюється на коректну HTTP‑відповідь і при цьому не ламає дані.

JSON‑тести вже довели стабільність DTO та error contract. Отже, regression‑тест не має повторювати golden‑JSON цілком. Він бере 2–4 поля, які реально доводять сенс сценарію: status, можливо errorCode, можливо slug/id, і все. Решта — це робота дешевих контрактних тестів.

MVC slice‑тести вже довели, що контролер правильно приймає й віддає потрібні формати, коректно обробляє валідацію та формує базові відповіді. Отже, regression‑тест не має заново тестувати «всі обмеження request DTO». Його завдання — довести, що запит не лише «прийняли», а й «правильно обробили до кінця ланцюжка».

Data‑slice тести довели, що мапінг і ключові репозиторні запити працюють. Отже, regression‑тест не має повторно перевіряти «як працює findBySlug у трьох варіантах сортування». Він використовує читання з репозиторію як інструмент підтвердження фінального стану.

Дуже допомагає така таблиця «де що живе», саме як антидубль:

Що ви хочете довести Де це дешевше й правильніше перевіряти Що regression‑тест залишає собі
Валідація request DTO (порожній title, довжини, формати) @WebMvcTest Зазвичай не повторює (крім пари дійсно критичних помилок).
Стабільність JSON‑контракту відповіді @JsonTest Бере кілька полів як підтвердження життя.
Матриця допустимих переходів статусів unit‑тести policy Бере 1–2 репрезентативні сценарії через HTTP + перевірку error contract.
Мапінг, constraints, ключові queries @DataJpaTest Використовує репозиторій як «свідка», щоб перевірити стан.
Наскрізний зв’язок шарів і вплив на public API повна інтеграційна регресія Ось це — центральна цінність.

Якщо ви ловите себе на думці «я зараз тестую в regression те, що можна протестувати в @WebMvcTest у 20 разів швидше», це майже завжди сигнал: ви будуєте красиву, але дорогоцінну копію того, що у вас уже є.

4. Структура suite за бізнес-потоками

Навіть ідеальні тести перетворюються на гарбуз, якщо їх неможливо швидко читати. А regression suite читають часто: коли він падає, коли реліз, коли хтось питає: «чому це вчора працювало?». Тому структура suite — не другорядна тема, а половина успіху.

Найстійкіший принцип: групувати regression‑тести за бізнес-сценаріями, а не за технічними інструментами. Не “SpringBootTest1/2/3”, а “SubmitFlow / PublishFlow / RejectFlow / ArchiveFlow”. Тоді навіть новачок, який не знає, що таке @AutoConfigureMockMvc, зможе очима знайти “publish”.

У Java‑коді це можна виразити по-різному. Один із варіантів — один клас із групуванням @Nested (це зручно, якщо ви хочете тримати єдиний контекст, спільні helpers і єдиний стиль). Другий варіант — кілька класів за потоками, якщо ви хочете фізично розділити файли. Обидва підходи нормальні; важливіше, щоб тести не розповзалися по випадкових назвах.

Мінімальний каркас «один клас = один suite» виглядає так:

import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.test.context.ActiveProfiles;

@SpringBootTest // Піднімаємо повний Spring-контекст для наскрізних сценаріїв
@AutoConfigureMockMvc // Дає MockMvc для HTTP-викликів без реального порту
@ActiveProfiles("test") // Загальне тестове середовище для regression suite
class PublicationRegressionSuiteTest {
    // Тут будуть лежати high-ROI наскрізні сценарії (регресійне ядро)
}

Якщо ви обираєте @Nested, то групування за потоками виходить дуже читабельним:

import org.junit.jupiter.api.Nested; // Групування сценаріїв усередині suite

class PublicationRegressionSuiteTest {

    @Nested
    class SubmitFlow {
        // Кроки сценарію: create draft -> submit for review
    }
}

І тут важливий момент: @Nested — не «щоб було модно», а щоб один великий дорогий suite не перетворився на плоский список із 30 методів, де ви гортаєте очима й думаєте: «а де тут publish?».

Якщо зібрати сьогоднішні flows у реальне ядро ContentHub, картина виходить досить компактною:

Що залишається в daily core Ключові checkpoints Чому це high-ROI Що свідомо не дублюємо
create draft → submit 201 Created, id, DRAFT → IN_REVIEW, фінальне читання стану Це вхідні двері робочого процесу редактора; ламається на стику controller/service/repository/moderation Повний JSON відповіді, всю security-матрицю
approve → publish PUBLISHED, publishedAt, public GET за slug повертає 200 Зв’язує адміністративний потік, стан даних і публічну видимість Увесь public DTO та весь JSON‑контракт
reject REJECTED, rejectionReason, стаття залишається непублічною Захищає нормальну негативну бізнес-гілку Усі деталі error/public contract навколо цього flow
archive ARCHIVED, стаття зникає з public API Захищає зняття матеріалу з публічної зони Повторну перевірку всього publish-flow цілком
Один репрезентативний invalid transition 409, errorCode, дані не змінилися Дає integration-proof для policy error path Усю матрицю статусних переходів
1 точковий live-server probe Реальний round-trip для вибраного flow Підсилює впевненість там, де важливий живий сервер Перетворення всього suite на RANDOM_PORT

Зверніть увагу: у цьому ядрі ми свідомо вважаємо, що запит робить допустимий editor/admin-користувач. 401/403, ролі та доступ на основі власника — окремий шар регресії. Якщо притягнути його в кожен business-flow тест вище, compact core зникне вже за перший тиждень.

5. Допоміжні методи: менше шуму, більше сенсу

Інтеграційні тести майже неминуче містять “транспортний шум”: побудову JSON‑рядків, HTTP‑виклики, витягування id, читання відповідей. Якщо писати все «в лоб» у кожному тесті, ви отримаєте полотна коду, які важко читати й неприємно лагодити. Якщо заховати все в «супер‑DSL на 50 методів», ви отримаєте магію, яка ламається й лякає.

Золота середина — короткі helpers, які роблять один зрозумілий крок сценарію або одну зрозумілу перевірку. Вони мають зменшувати шум, але залишати сценарій читабельним як історію: “створили → відправили → перевірили”.

Приклад “здорового” helper-а для повторюваної доменної перевірки:

private void assertArticleStatus(long id, ArticleStatus expected) {
    // Репозиторій тут виступає «свідком» фінального стану в БД
    Article article = articleRepository.findById(id).orElseThrow(); // Якщо статті немає — це теж падіння сценарію
    assertThat(article.getStatus()).isEqualTo(expected); // Перевіряємо ключовий бізнес-ефект
}

Він не ховає сенс, а, навпаки, робить його виразнішим: у тесті залишається “assert status”, а не “дістань entity, перевір поле”.

А ось helper-и для кроків сценарію мають бути ще акуратнішими: якщо createDraft() усередині робить три запити, змінює профіль і ще готує SQL, то у вас зникає «історія тесту». Хороший helper про крок сценарію зазвичай робить один HTTP‑виклик і повертає один артефакт (наприклад, id або slug).

Навіть витягування id можна тримати коротким, не перетворюючи тест на «парсер відповідей»:

private long extractId(String responseJson) {
    // Дістаємо лише те, що потрібно сценарію (не перетворюємо regression на JSON-енциклопедію)
    Number id = JsonPath.read(responseJson, "$.id");
    return id.longValue(); // JsonPath може повернути Integer/Long — приводимо безпечно
}

Якщо ви в якийсь момент ловите себе на тому, що тест читається як: givenDraft().withFoo().withBar().submit().approve().publish().assertEverything() — це майже завжди означає, що ви побудували DSL заради самого DSL. Regression suite не має бути “красивішим за production-код”. Він має бути простішим, прямішим і чеснішим.

6. Бюджет контекстів і ціна suite

Regression suite — це не лише про кількість тестів, а й про кількість контекстів, які ви змушуєте Spring піднімати. Два тести можуть виглядати однаково “дорого” (обидва @SpringBootTest), але один запускатиметься 5 с, а другий 25 с — тому що ви непомітно створили три різні гілки конфігурації, і Spring тепер кешує три різні ApplicationContext.

Тому, коли ви збираєте компактний regression suite, думайте не лише “скільки тестів”, а й “скільки варіантів контексту”. Ідеальна ситуація для daily regression — коли основна частина тестів живе в одному профілі та в одному наборі властивостей, а гілки конфігурації винесені в окремі класи строго по справі.

Хороший базовий крок — тримати @ActiveProfiles("test") прямо в цьому скелеті поруч із @SpringBootTest і @AutoConfigureMockMvc. Тоді test‑profile перестає бути пізньою випадковою правкою і стає частиною домовленості про середовище.

Якщо вам потрібна окрема гілка конфігурації, нехай це буде окремий test class, а не “усередині одного класу ми іноді змінюємо властивості”. Тоді ви заздалегідь приймаєте ціну другого контексту, і у вас не з’являється сюрпризу “чому suite став удвічі повільнішим”.

Ще один момент, який несподівано руйнує бюджет регресії, — звичка лагодити нестабільність через @DirtiesContext. Так, це іноді працює. Але ціна у “давайте кожен тест буде забруднювати контекст” така, що regression suite перестає бути щоденним. Краще вкластися в детерміновані дані, явний @Sql і передбачувані налаштування, ніж оплачувати проблему повним пересозданням світу після кожного методу.

7. Live-server тести: випадки застосування

Live-server тести виглядають дуже “справжніми”: справжній порт, справжній HTTP‑клієнт, майже як “прод”. І саме тому є спокуса сказати: “ну раз справжній — давайте все перенесемо туди”. Це небезпечна ідея, тому що live-server режим майже завжди дорожчий за часом і складніший за діагностикою.

Правильний підхід: залишити live-server тести точково, там, де вони реально дають унікальний доказ порівняно з full-context + MockMvc. Наприклад, коли ви хочете підтвердити саме client‑side поведінку (як клієнт бачить URL, заголовки, серіалізацію) у реальному піднятому сервері.

Каркас такого тесту зазвичай виглядає максимально нудно — і це добре:

import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.autoconfigure.web.client.AutoConfigureRestTestClient;

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) // Реальний порт і справжній HTTP-раундтрип
@AutoConfigureRestTestClient // Піднімаємо тестовий HTTP-клієнт поверх піднятого сервера
class PublicVisibilityLiveServerRegressionTest {
}

Зверніть увагу: ми не робимо з цього “головний suite”. Ми робимо один‑два тести, які додають упевненості там, де mocked MVC за визначенням не може дати 100% того самого відчуття “реального раундтрипу”.

Якщо ж ваш live-server тест повторює той самий набір перевірок, що у вас уже є на MockMvc full context, то він найчастіше не regression‑ядро, а просто дорога копія.

8. Типові помилки під час побудови компактного regression suite

Помилка № 1: вимірювати якість кількістю integration‑тестів.
Дуже хочеться сказати: “У нас 40 наскрізних тестів — отже, ми молодці”. На практиці це часто означає, що у вас 40 способів отримати повільний build і 40 місць, де тест може впасти через недетерміновані дані. Професійний suite вимірюється корисним сигналом, а не кількістю зелених галочок.

Помилка № 2: перетворювати regression‑тест на «енциклопедію всіх assertions».
Коли тест перевіряє і статус, і всі поля JSON, і всі заголовки, і всі обмеження валідації, і всі побічні ефекти, він стає погано діагностованим. Падає такий тест неприємно: незрозуміло, що реально зламалося. Набагато сильніше працює підхід із кількома контрольними точками regression, де кожна точка доводить окремий спостережуваний результат.

Помилка № 3: дублювати те, що вже закрито дешевшими шарами.
Якщо unit‑тести вже довели матрицю переходів, @JsonTest уже тримає контракт DTO, @WebMvcTest уже тримає валідацію та error contract, то regression не зобов’язаний повторювати все це в повному обсязі. Він має довести саме те, що дешеві тести не доведуть: зв’язок шарів і зовнішній ефект.

Помилка № 4: занадто розумні helpers, які ховають сценарій.
Helpers корисні, але вони мають залишати кроки сценарію видимими. Якщо всередині publishArticle() заховані create, submit, approve, читання з БД, перевірка public API і ще п’ять деталей, тест перетворюється на “викликали магію — отримали результат”. Така магія погано лагодиться і погано навчає.

Помилка № 5: змішувати кілька гілок конфігурації в одному класі або навіть в одному тесті.
Це класичний шлях до flaky: сьогодні тест пішов однією гілкою, завтра — іншою, післязавтра контекст пересобрався через локальний override. Одна гілка конфігурації має бути представлена одним класом і читатися з анотацій, як заголовок книги: “це suite для ось такої поведінки”.

1
Задача
Spring Test, 22 рівень, 4 лекція
Недоступна
Компактний regression suite за бізнес-потоками
Компактний regression suite за бізнес-потоками
1
Задача
Spring Test, 22 рівень, 4 лекція
Недоступна
Один live-server probe для public endpoint
Один live-server probe для public endpoint
1
Опитування
Регресійні тести, рівень 22, лекція 4
Недоступний
Регресійні тести
Наскрізні перевірки бізнес-потоків
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ