Перейти к основному содержимому

JUnit 5 и тестирование Java

Разработчику Архитектору

JUnit 5 и тестирование Java

Тесты — это способ не бояться менять код. Хороший набор тестов отвечает на вопрос: "Я сломал что-то важное?" — за секунды, до деплоя.

В Java-мире базовая тройка такая:

УровеньИнструментЧто проверяем
UnitJUnit 5 + MockitoОдин класс, зависимости подменены
Web-слойMockMvcКонтроллер без поднятия Tomcat
Интеграция@SpringBootTest, TestcontainersКонтекст + БД в Docker

JUnit 5 (модуль Jupiter) — стандарт unit-тестов. Сборка: Maven / Gradle. Spring: Первая программа на Spring Framework, Security, JWT, ошибки API.

С чего начать

Напишите тест на чистую функцию (Calculator ниже), запустите mvn test, только потом подключайте Mockito и MockMvc.


JUnit 5 — основы

Зависимость Maven:

<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>

Разбор:

  • этот XML-блок описывает конфигурацию сборки, которую инструмент считывает до выполнения прикладного Java-кода.
  • элементы с артефактами junit-jupiter включают нужные библиотеки и определяют доступные API в кодовой базе.
  • секция <scope>test</scope> ограничивает зависимость тестовым контуром и не тащит ее в production-артефакт.
  • любое изменение здесь влияет на classpath и поведение сборки, поэтому такие правки проверяют полным прогоном тестов.
  • после редактирования этого фрагмента проверяют, что проект собирается и тестовый раннер видит нужные классы без конфликтов версий.

Код ITЗагрузка примера кода…

Запуск:

mvn test
# или
./mvnw test

Разбор:

  • команды в этом фрагменте выполняются по шагам и составляют практический workflow — запуск, тесты, сборка или проверка окружения.
  • последовательность mvn test, ./mvnw test покрывает ключевые этапы жизненного цикла проекта в CLI.
  • если одна команда завершается ошибкой, последующие шаги не подтверждают корректность сценария, пока не устранена первопричина.
  • результат смотрят по exit code и по логам команды: успешный статус, ожидаемые артефакты и отсутствие stack trace.
  • такой же набор команд обычно фиксируют в CI, чтобы локальная проверка и pipeline давали одинаковый результат.

Разбор аннотаций:

  • @Test — метод-тест; без него JUnit не запустит метод.
  • @DisplayName — человекочитаемое имя в отчёте IDE/CI.
  • @ParameterizedTest + @CsvSource — один тест, много входных строк (меньше копипасты).
  • assertThrows — ожидаемое исключение (важно для валидации и деления на ноль).

Класс Calculator для примера — обычный Java без Spring; тесты лежат в src/test/java в том же пакете.


Mockito

<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<scope>test</scope>
</dependency>

Разбор:

  • этот XML-блок описывает конфигурацию сборки, которую инструмент считывает до выполнения прикладного Java-кода.
  • элементы с артефактами mockito-junit-jupiter включают нужные библиотеки и определяют доступные API в кодовой базе.
  • секция <scope>test</scope> ограничивает зависимость тестовым контуром и не тащит ее в production-артефакт.
  • любое изменение здесь влияет на classpath и поведение сборки, поэтому такие правки проверяют полным прогоном тестов.
  • после редактирования этого фрагмента проверяют, что проект собирается и тестовый раннер видит нужные классы без конфликтов версий.

Код ITЗагрузка примера кода…

Разбор: @Mock — заглушка; @InjectMocks — класс под тестом, в него подставят моки. when(...).thenReturn(...) задаёт поведение; verify проверяет, что метод вызвали. Так тестируют сервис без реальной БД.


Spring Boot Test

В проекте Spring Boot достаточно spring-boot-starter-test (JUnit + Mockito + AssertJ + MockMvc).


@WebMvcTest — контроллер изолированно

Поднимается только веб-слой (контроллер + JSON), без полной БД. Быстро и подходит для проверки Ошибки REST — @Valid и @ControllerAdvice — валидации.

@WebMvcTest(HelloController.class)
class HelloControllerTest {

@Autowired
MockMvc mvc;

@Test
void returnsGreeting() throws Exception {
mvc.perform(get("/api/hello").param("name", "Java"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.message").value("Hello, Java"));
}
}

jsonPath — как XPath для JSON: достаёт поле из тела ответа.


@SpringBootTest — полный контекст

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class ApplicationIT {

@Autowired
TestRestTemplate rest;

@Test
void health() {
var response = rest.getForEntity("/actuator/health", String.class);
assertEquals(200, response.getStatusCode().value());
}
}

Структура тестов Maven

src/
main/java/...
test/java/... # зеркало пакетов main

Имена: *Test.java (unit), *IT.java (integration, опционально отдельный профиль).


CI

- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '21'
- run: mvn -B verify

Тестовая пирамида на практике

Удобная стратегия для реального проекта:

  • много unit-тестов (JUnit 5 + Mockito) — быстрый feedback на бизнес-логику;
  • меньше web-тестов (MockMvc) — проверка контрактов контроллеров;
  • немного интеграционных тестов (@SpringBootTest, Testcontainers) — проверка "склейки" с БД и инфраструктурой.

Если пирамида перевёрнута (почти всё на @SpringBootTest), сборка становится медленной и команда начинает реже запускать тесты локально.


Тестирование конкурентного кода

Для ExecutorService, CompletableFuture и логики с общим изменяемым состоянием нужны отдельные проверки. Иначе ошибки синхронизации часто проявляются только под нагрузкой.

Минимальный набор:

  1. Тест на таймаут (orTimeout, get(timeout, ...)) для внешних вызовов.
  2. Тест на отмену (future.cancel(true)) и корректную обработку InterruptedException.
  3. Повторяемый стресс-кейс (цикл запусков) для поиска race condition.

Практика стабильности:

  • избегайте "голых" Thread.sleep(...) без явного условия;
  • для ожиданий лучше использовать await-подход и фиксированные дедлайны;
  • если тест "плавает", сначала уберите зависимость от реального времени и планировщика ОС.

Расшифровка терминов:

  • Race condition — итог зависит от случайного порядка выполнения потоков.
  • Flaky test — тест иногда падает без изменений в коде.
  • Deadline — максимальное время ожидания, после которого тест завершает шаг с ошибкой.

Связанные главы:


Минимальный план для нового модуля

  1. На каждый публичный метод сервиса — 2-5 unit-тестов (happy path + edge cases).
  2. На каждый внешний HTTP-контракт — минимум один MockMvc тест со статусом и JSON-полями.
  3. На каждый репозиторий или SQL-слой — 1-2 интеграционных теста на реальной БД.
  4. На критический пользовательский сценарий — один end-to-end тест в CI.

Частые антипаттерны и как исправлять

АнтипаттернПочему плохоЧто делать
Один большой тест "проверяет всё сразу"Трудно понять, что сломалосьДелить на узкие тесты по одному поведению
Проверка только status().isOk()Контракт ответа может сломаться незаметноДобавлять jsonPath и проверять ключевые поля
Много Thread.sleep(...) в тестахНестабильность и долгий прогонИспользовать await-подходы (Awaitility) и явные условия
Моки для всего, включая слой БДНе видно проблем SQL/маппингаДобавить интеграционные тесты с Testcontainers
Случайные данные без фиксированного seedТесты "плавают"Фиксировать seed и значения входов
Связанные материалы

Для API-валидации и ошибок комбинируйте подходы из Ошибки REST, Spring Security и JWT.


Частые ошибки

СимптомПричина
Тесты не запускаютсяЗависимость без scope=test
NullPointerException в тесте@InjectMocks без @Mock для зависимости
MockMvc 404Не тот @WebMvcTest(Controller.class)
Медленные "unit"-тестыПоднят лишний @SpringBootTest

Что попробовать

  1. Тест на глобальный обработчик ошибокPOST с пустым телом → 400.
  2. @WithMockUser вместе с Security для закрытого эндпоинта.
  3. Testcontainers — один IT на репозиторий.

Дальше

Testcontainers · JWT + Security · Gradle · Spring Boot