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 появился ровно как лекарство от такого dependency-хаоса: он даёт согласованный набор библиотек и их совместимые версии.

Важно правильно понять слово starter. Это не «магическая библиотека, которая делает тесты качественными». Стартер — это, по сути, удобный список зависимостей, который Spring Boot умеет поддерживать как цельный набор. Он решает инженерную проблему воспроизводимости: вы не обязаны каждый раз заново выбирать версии JUnit/Mockito/AssertJ и переживать, что один модуль подтянул одно, а другой — другое. В мире backend-разработки это сильно недооценённая ценность: пока тесты зелёные, кажется, что всё просто; когда CI падает из‑за конфликтов в classpath — хочется вернуться в прошлое и сказать себе «не трогай версии руками».

Ещё один важный момент: spring-boot-starter-test не превращает ваш проект в «проект, где всё тестируется через Spring». Он не отменяет unit-тесты без Spring, которые мы строили в предыдущих днях. Наоборот: стартер лишь гарантирует, что когда вам понадобятся Spring-specific инструменты, они будут рядом и совместимы. А вот выбор уровня теста (unit vs slice vs full context) остаётся инженерным решением, а не эффектом подключения зависимости.

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 — вы не словили «случайный апгрейд» одной библиотеки из пяти.

Это не означает, что версии нельзя override вообще никогда. Но для учебного проекта (и для большинства реальных проектов, если честно) правило простое: не ломайте платформу без причины. Если вы дописываете в build.gradle.kts что-то вроде «я тут JUnit поновее поставлю», вы легко можете получить набор тестов, который вроде бы компилируется, но ведёт себя странно. Такой странности обычно достаточно, чтобы полдня спорить с Gradle и ещё полдня спорить с собой.

3. Состав spring-boot-starter-test

Когда мы говорим «стартер включает тестовый стек», очень хочется воспринимать это как чёрный ящик: «ну там какая-то магия, тесты работают». Но для хорошего тестирования важнее другое: понимать роли. Представьте, что тест — это маленький спектакль. Один актёр запускает действие, другой проверяет результат, третий подменяет партнёров по сцене, а ещё есть человек, который открывает театр, включает свет и запускает оркестр. Если перепутать роли, вы получите либо хаос, либо очень странный перформанс.

Ниже — компактная карта того, что обычно скрывается за spring-boot-starter-test в Boot-centered мире:

Компонент в стеке Что это такое простыми словами За что отвечает в тестах Пример того, как вы его «видите»
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[Test class] --> J[JUnit Jupiter]
  T --> A[AssertJ]
  T --> M[Mockito]
  T --> ST[Spring Test]
  ST --> BT[Spring Boot Test]
  BT -->|поддержка| Anno[Boot test annotations]

Заметьте, что 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», он просто делает единый baseline доступным. Вы выбираете уровень теста осознанно, а не потому что «так принято в туториале».

Теперь небольшой контраст: вот тест, который уже опирается на Boot test ecosystem. Сам по себе он тоже ничего не доказывает кроме факта запуска контекста, но он демонстрирует, что инфраструктура 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. Да, хороший инструмент помогает, но он не делает за вас выбор, что именно утверждать. Точно так же он не лечит от over-mocking: Mockito может быть в classpath идеально совместимым, но если вы замокали половину мира, вы всё равно построили хрупкую иллюзию, а не проверку.

И последнее: стартер не равен «всё включено». Он покрывает базовые нужды, но в реальном приложении обычно есть тестовые зависимости, которые подключаются отдельно, потому что они относятся к специфическим зонам риска (например, безопасность или контейнерный запуск реальной БД). Важно лишь, чтобы такие вещи подключались осознанно и не ломали целостность платформы. Но сама мысль должна быть спокойной: стартер — это база, а не свалка всего тестового мира.

6. Типичные ошибки при работе со spring-boot-starter-test

Ошибка №1: подключить стартер, а потом добавить те же библиотеки руками “для надёжности”.
Так обычно и начинается «версионный ад». Например, в проекте уже есть spring-boot-starter-test, но кто-то добавляет junit-jupiter отдельной строкой, ещё и с версией «какую нашёл в статье». В итоге у вас два источника правды о версиях: Boot platform и ваш ручной override. Иногда оно даже компилируется, но ведёт себя непредсказуемо, а падение будет выглядеть как «что-то странное внутри тестового движка». Лечится это скучно: доверять платформе и не дублировать зависимости.

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

Ошибка №3: считать, что появление стартера означает “все тесты теперь Spring-тесты”.
Это тихая деградация проекта. Вы начнёте поднимать Spring там, где это не нужно, тесты замедлятся, а обратная связь станет хуже. Самое обидное: скорость падает постепенно, и вы привыкаете. В итоге вы будете бояться запускать тесты локально, а это уже почти диагноз для инженерной культуры. Правильная мысль: стартер включён, но unit-тест остаётся нормой для локальной логики.

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

Ошибка №5: пытаться “починить” плохую стратегию тестов ещё большим количеством зависимостей.
Иногда кажется: «всё плохо, добавим ещё одну библиотеку». Но стартер — не про количество. Он про базу. Если тесты хрупкие, причина чаще всего в границах ответственности, в недетерминизме, в неправильных проверках или в том, что вы тестируете не тот слой. Сначала лечим причины, а потом уже добавляем инструменты, если они действительно нужны.

1
Задача
Spring Test, 6 уровень, 0 лекция
Недоступна
Plain unit-test на baseline `spring-boot-starter-test`
Plain unit-test на baseline `spring-boot-starter-test`
1
Задача
Spring Test, 6 уровень, 0 лекция
Недоступна
Mockito из starter без отдельных зависимостей
Mockito из starter без отдельных зависимостей
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ