JavaRush /Курси /Spring Test /spring-boot-starter-test

spring-boot-starter-test для тестування

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

1. Призначення spring-boot-starter-test

Якщо ви колись намагалися «по-чесному» зібрати тестовий стек власноруч, ви вже знаєте це відчуття: начебто хочеться просто написати тест, а насправді ви раптом вивчаєте археологію версій. «Який JUnit?», «Який Mockito?», «А AssertJ із ним дружить?», «А який Spring Test потрібен нашому Spring?» — і ось ви вже не тестуєте, а проводите ритуал викликання сумісності. spring-boot-starter-test зʼявився саме як ліки від такого хаосу залежностей: він дає узгоджений набір бібліотек і їхні сумісні версії.

Важливо правильно зрозуміти слово starter. Це не «магічна бібліотека, яка робить тести якісними». Стартер — це, по суті, зручний список залежностей, який Spring Boot уміє підтримувати як цілісний набір. Він розв’язує інженерну проблему відтворюваності: вам не доводиться щоразу заново обирати версії JUnit, Mockito чи AssertJ і хвилюватися, що один модуль підтягнув одне, а інший — інше. У світі бекенд-розробки це дуже недооцінена цінність: доки тести зелені, здається, що все просто; коли CI падає через конфлікти в classpath, хочеться повернутися в минуле й сказати собі: «Не чіпай версії руками».

Ще один важливий момент: spring-boot-starter-test не перетворює ваш проєкт на «проєкт, де все тестується через Spring». Він не скасовує unit-тести без Spring, які ми будували в попередні дні. Навпаки: стартер лише гарантує, що коли вам знадобляться інструменти, специфічні для Spring, вони будуть поруч і сумісні. А ось вибір рівня тесту — unit, slice чи повний контекст — залишається інженерним рішенням, а не наслідком підключення залежності.

2. Підключення в Gradle

У реальному проєкті найчастіше помилки починаються не в коді тесту, а в build.gradle.kts: хтось додав JUnit окремо «про всяк випадок», хтось указав версію Mockito «як у туторіалі 2019 року», а потім проєкт загадково компілюється в однієї людини й так само загадково ламається в іншої. Сила Spring Boot як платформи в тому, що він дає curated dependency set: фіксований набір версій, перевірених на сумісність між собою. Щоб цим скористатися, spring-boot-starter-test підключається максимально нудно — і це якраз добре.

Типовий мінімум для Gradle-проєкту виглядає так (формат навмисно короткий, без зайвого шуму):

plugins {
    // Підключаємо Java-плагін для стандартних завдань компіляції та тестування.
    java

    // Плагін Spring Boot підтягне узгоджене керування версіями для стартера.
    id("org.springframework.boot") version "4.0.3"
}

dependencies {
    // Стартер потрібен лише тестам: у production classpath він потрапляти не повинен.
    testImplementation("org.springframework.boot:spring-boot-starter-test")
}

tasks.test {
    // Тести запускаємо на платформі JUnit (у курсі — JUnit Jupiter 6).
    useJUnitPlatform()
}

Тут важливе слово testImplementation. Воно означає: «ця залежність потрібна лише для тестів». У production-код (src/main/java) вона не потрапляє, у ваш runtime-jar теж. Це майже як окрема комірка, куди ви складаєте молоток, викрутку та ізострічку, але не носите їх постійно в офісному костюмі, хоча деяких розробників це не зупиняє.

Тепер про версії. У цьому курсі в нас зафіксовано baseline: Spring Boot 4.0.3, Spring Framework 7.0.5, JUnit Jupiter 6.0.3, AssertJ 3.27.7, Mockito 5.20.0. І ідея Boot-платформи в тому, що ви зазвичай не пишете ці версії руками в dependencies { ... } для бібліотек, які вже входять до керованого стеку. Boot сам підтягне узгоджені версії через своє dependency management. Так ви отримуєте стабільність: якщо оновили Boot — оновився весь набір узгоджено; якщо не оновлювали Boot — не зловили «випадковий апгрейд» однієї бібліотеки з пʼяти.

Це не означає, що версії не можна перевизначати взагалі ніколи. Але для навчального проєкту, та й для більшості реальних проєктів, якщо чесно, правило просте: не ламайте платформу без причини. Якщо ви дописуєте в build.gradle.kts щось на кшталт «я тут JUnit поновішу поставлю», ви легко можете отримати набір тестів, який начебто компілюється, але поводиться дивно. Такої дивини зазвичай достатньо, щоб пів дня сперечатися з Gradle і ще пів дня — із собою.

3. Склад spring-boot-starter-test

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

Нижче — компактна карта того, що зазвичай ховається за spring-boot-starter-test у світі, орієнтованому на Boot:

Компонент у стеку Що це таке простими словами За що відповідає в тестах Приклад того, як ви його «бачите»
JUnit Jupiter тестовий рушій запускає тестові методи, керує життєвим циклом @Test, @BeforeEach, @Nested
AssertJ мова перевірок робить твердження читабельними, а падіння — зрозумілими assertThat(result).isEqualTo(...)
Mockito (+ junit-jupiter інтеграція) мокування/стаби допомагає ізолювати залежності там, де це виправдано @ExtendWith(MockitoExtension.class)
Hamcrest matchers (часто legacy) інколи використовується в екосистемі Spring-тестів hasSize(3) та подібні
JSONassert порівняння JSON дає змогу порівнювати JSON структурно, а не посимвольно «у JSON є поле status»
JsonPath вибірка даних із JSON дає змогу точково перевіряти шматки JSON $.data[0].title
Spring Test міст між тестом і Spring надає TestContext framework, MVC-тести, утиліти @ExtendWith(SpringExtension.class) (часто неявно)
Spring Boot Test Boot-надбудова над Spring Test допомагає підіймати контекст Boot і користуватися тестовими анотаціями Boot @SpringBootTest, @JsonTest, @WebMvcTest тощо

Якщо говорити простіше: JUnit відповідає за «коли і що запустити», AssertJ — за «як красиво й точно перевірити», Mockito — за «як замінити залежність», а Spring Test і Spring Boot Test — за те, як акуратно підключити Spring/Boot там, де це справді потрібно. JSONassert і JsonPath — це інструменти, які дають змогу перевіряти JSON як структуру, а не як «рядок, який випадково збігся цього вівторка».

Корисно побачити це ще й як схему, без зайвої деталізації, щоб не втекти в наступні лекції:

flowchart LR
  %% Спрощена карта: хто чим займається в типовому тесті Boot-проєкту.
  T[Тестовий клас] --> J[JUnit Jupiter]
  T --> A[AssertJ]
  T --> M[Mockito]
  T --> ST[Spring Test]
  ST --> BT[Spring Boot Test]
  BT -->|підтримка| Anno[Boot-анотації для тестів]

Зверніть увагу, що spring-boot-starter-test — це саме збірка інструментів. Він не говорить: «ось так має виглядати ваш тест». Він лише гарантує, що всі ці шматочки, коли ви почнете ними користуватися, не будуть битися за classpath, як два коти на кухні.

4. Unit-тести поруч із Spring-тестами

Є дуже поширена психологічна пастка: щойно в проєкті зʼявляється Spring Boot, здається, що «все має бути зі Spring». Щойно зʼявляється spring-boot-starter-test, виникає спокуса: «раз уже в тестах є Spring — давайте завжди підіймати контекст, так надійніше». І ось так у тести непомітно проникає повільне й дороге тестування, хоча взагалі-то половину ризиків ми вже прекрасно ловили unit-тестами без Spring. Тому зараз важлива фіксація: стартер підключений — але Spring-контекст не обов’язковий.

Подивімося на простий unit-тест із нашого домену. Тут перевіряється бізнес-правило, і воно не повинно залежати від контейнера:

import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;

class PublicationPolicyTest {

    @Test
    void allowsDraftToMoveToReview() {
        // Створюємо доменний об’єкт напряму — без Spring і без контексту.
        PublicationPolicy policy = new PublicationPolicy();

        // Перевіряємо саме бізнес-правило (поведінку), а не внутрішності реалізації.
        assertThat(policy.canMove(ArticleStatus.DRAFT, ArticleStatus.IN_REVIEW)).isTrue();
    }
}

Це звичайний JUnit плюс AssertJ, жодних Spring-анотацій. І при цьому тест чудово живе в Boot-проєкті з підключеним spring-boot-starter-test. Тут і проявляється головний практичний сенс стартера: він не змушує вас жити лише в Spring, він просто робить єдиний базовий набір доступним. Ви обираєте рівень тесту свідомо, а не тому, що «так прийнято в туторіалі».

Тепер невеликий контраст: ось тест, який уже спирається на екосистему Boot-тестування. Сам по собі він теж нічого не доводить, крім факту запуску контексту, але демонструє, що інфраструктура Spring-specific тестування береться «з коробки» зі стартером:

import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class ContentHubApplicationSmokeTest {

    @Test
    void contextLoads() {
        // Smoke-тест: якщо контекст не підніметься, тест упаде ще до виконання тіла методу.
    }
}

Два тести — два зовсім різні сенси, і це нормально. Перший — дешевий і точковий, другий — інфраструктурний і дорогий. Стартер підтримує обидва, але не робить вибір за вас.

І ще одна маленька демонстрація «батарейка вже всередині»: Mockito у вас теж на місці, без окремого підключення. Навіть якщо код нижче вам здається знайомим (ми вже працювали з Mockito раніше), тут важливо саме те, що вам не довелося додавати окрему залежність заради MockitoExtension:

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;

import org.mockito.junit.jupiter.MockitoExtension;

@ExtendWith(MockitoExtension.class)
class MockitoBaselineTest {

    @Test
    void mockitoIsAvailable() {
        // Створюємо мок: реальна логіка нам тут не потрібна, важливий факт доступності Mockito.
        Object dep = mock(Object.class);

        // Проста перевірка, що мок успішно створено і бібліотеку підключено.
        assertThat(dep).isNotNull();
    }
}

Так, це тест «на існування Mockito», і в реальному проєкті так не роблять. Але як навчальний «клацок розуміння» він корисний: стартер уже притягнув те, що вам потрібно, і не попросив за це окремої церемонії.

5. Що стартер не робить

Є небезпечний міф: «підключу spring-boot-starter-test, і тестування стане на рейки». Це приблизно як купити набір інструментів і очікувати, що кухня сама збереться. Стартер розв’язує проблему сумісності та базового набору, але він не замінює інженерне мислення, заради якого взагалі будується цей курс.

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

Іще стартер не розв’язує проблему якості самих перевірок. Якщо ви пишете тест, який перевіряє внутрішні деталі реалізації замість поведінки, це не стане кращим від того, що ви використовуєте версію AssertJ 3.27.7 замість 3.24.2. Так, добрий інструмент допомагає, але він не робить за вас вибір, що саме стверджувати. Точно так само він не лікує від надмірного мокування: Mockito може бути в classpath ідеально сумісним, але якщо ви замокали половину світу, ви все одно побудували крихку ілюзію, а не перевірку.

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

6. Типові помилки під час роботи зі spring-boot-starter-test

Помилка №1: підключити стартер, а потім додати ті самі бібліотеки вручну «для надійності».
Так зазвичай і починається «версійне пекло». Наприклад, у проєкті вже є spring-boot-starter-test, але хтось додає junit-jupiter окремим рядком, ще й із версією, яку знайшов у статті. У підсумку у вас два джерела правди про версії: платформа Boot і ваше ручне перевизначення. Іноді воно навіть компілюється, але поводиться непередбачувано, а падіння виглядатиме як «щось дивне всередині тестового рушія». Лікується це нудно: довіряти платформі й не дублювати залежності.

Помилка №2: поставити стартер у implementation, а не в testImplementation.
На перший погляд здається: «ну яка різниця, хай буде доступний усюди». Різниця в тому, що ви збільшуєте production classpath, можете випадково протягнути тестові бібліотеки в runtime і ускладнюєте підтримку залежностей. У проді вам не потрібен Mockito, як би інколи не хотілося замокати реальність. Стартер має жити в тестовому скоупі.

Помилка №3: вважати, що поява стартера означає “усі тести тепер Spring-тести”.
Це тиха деградація проєкту. Ви почнете підіймати Spring там, де це не потрібно, тести сповільняться, а зворотний зв’язок стане гіршим. Найприкріше: швидкість падає поступово, і ви звикаєте. У підсумку ви будете боятися запускати тести локально, а це вже майже діагноз для інженерної культури. Правильна думка: стартер увімкнено, але unit-тест залишається нормою для локальної логіки.

Помилка №4: плутати ролі інструментів і писати “перевірки заради перевірок”.
Наприклад, JUnit не «перевіряє», він запускає. AssertJ не «мокає», він стверджує. Mockito не «доводить бізнес-правило», він допомагає ізолювати залежність. Коли ролі плутаються, тест перетворюється на набір випадкових рядків коду: десь assertEquals, десь verify, десь Spring-анотація «щоб усе працювало». Лікується це поверненням до простої моделі: хто запускає, хто перевіряє, хто ізолює, хто підіймає інфраструктуру.

Помилка №5: намагатися “полагодити” погану стратегію тестів ще більшою кількістю залежностей.
Інколи здається: «усе погано, додамо ще одну бібліотеку». Але стартер — не про кількість. Він про базу. Якщо тести крихкі, причина найчастіше в межах відповідальності, у недетермінізмі, у неправильних перевірках або в тому, що ви тестуєте не той шар. Спочатку лікуємо причини, а потім уже додаємо інструменти, якщо вони справді потрібні.

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