JUnit 5 и тестирование Java
JUnit 5 и тестирование Java
Тесты — это способ не бояться менять код. Хороший набор тестов отвечает на вопрос: "Я сломал что-то важное?" — за секунды, до деплоя.
В Java-мире базовая тройка такая:
| Уровень | Инструмент | Что проверяем |
|---|---|---|
| Unit | JUnit 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Загрузка примера кода…
Разбор:
- фрагмент показывает прикладной сценарий по теме раздела: здесь есть входные данные, операции над ними и проверяемый итог.
- аннотации
Test,DisplayName,ParameterizedTest,CsvSourceвключают поведение фреймворка (тестовый раннер, web-маршрутизация, ORM или транзакционные правила). - ключевые вызовы
Calculator,DisplayName,adds,assertEquals,add,CsvSource,addsParameterizedзадают основную механику этого примера: обработку, проверку и формирование результата. - проверки через
assert...фиксируют ожидаемое поведение как контракт, который автоматически проверяется на каждом прогоне тестов. - после запуска стоит проверить именно эффект этого примера — вывод, значение переменной, статус ответа, состояние БД или результат теста.
- при переносе в production обычно добавляют валидацию входа, явную обработку ошибок и тест на граничные случаи вокруг этого сценария.
Запуск:
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Загрузка примера кода…
Разбор:
- фрагмент показывает прикладной сценарий по теме раздела: здесь есть входные данные, операции над ними и проверяемый итог.
- аннотации
ExtendWith,Mock,InjectMocks,Testвключают поведение фреймворка (тестовый раннер, web-маршрутизация, ORM или транзакционные правила). - ключевые вызовы
ExtendWith,createsUser,when,existsByEmail,thenReturn,save,anyзадают основную механику этого примера: обработку, проверку и формирование результата. - проверки через
assert...фиксируют ожидаемое поведение как контракт, который автоматически проверяется на каждом прогоне тестов. - после запуска стоит проверить именно эффект этого примера — вывод, значение переменной, статус ответа, состояние БД или результат теста.
- при переносе в production обычно добавляют валидацию входа, явную обработку ошибок и тест на граничные случаи вокруг этого сценария.
Разбор: @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"));
}
}
Разбор:
- фрагмент показывает прикладной сценарий по теме раздела: здесь есть входные данные, операции над ними и проверяемый итог.
- аннотации
WebMvcTest,Autowired,Testвключают поведение фреймворка (тестовый раннер, web-маршрутизация, ORM или транзакционные правила). - ключевые вызовы
WebMvcTest,returnsGreeting,perform,get,param,andExpect,statusзадают основную механику этого примера: обработку, проверку и формирование результата. - чтение этого блока по шагам дает предсказуемую картину — сначала подготовка данных, затем действие API, затем получение конечного значения.
- после запуска стоит проверить именно эффект этого примера — вывод, значение переменной, статус ответа, состояние БД или результат теста.
- при переносе в production обычно добавляют валидацию входа, явную обработку ошибок и тест на граничные случаи вокруг этого сценария.
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());
}
}
Разбор:
- фрагмент показывает прикладной сценарий по теме раздела: здесь есть входные данные, операции над ними и проверяемый итог.
- аннотации
SpringBootTest,Autowired,Testвключают поведение фреймворка (тестовый раннер, web-маршрутизация, ORM или транзакционные правила). - ключевые вызовы
SpringBootTest,health,getForEntity,assertEquals,getStatusCode,valueзадают основную механику этого примера: обработку, проверку и формирование результата. - проверки через
assert...фиксируют ожидаемое поведение как контракт, который автоматически проверяется на каждом прогоне тестов. - после запуска стоит проверить именно эффект этого примера — вывод, значение переменной, статус ответа, состояние БД или результат теста.
- при переносе в production обычно добавляют валидацию входа, явную обработку ошибок и тест на граничные случаи вокруг этого сценария.
Структура тестов Maven
src/
main/java/...
test/java/... # зеркало пакетов main
Разбор:
- фрагмент показывает рабочий пример по теме и задает базовый шаблон для собственного кода.
- строки выполняются последовательно: подготовка данных, применение API и получение результата.
- ключевые конструкции этого блока стоит воспринимать как каркас, который дополняется доменной логикой.
- после запуска проверяют, что фактический результат совпадает с ожидаемым сценарием.
- в production-версии дополнительно закрывают граничные случаи и добавляют проверку ошибок.
Имена: *Test.java (unit), *IT.java (integration, опционально отдельный профиль).
CI
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '21'
- run: mvn -B verify
Разбор:
- фрагмент показывает рабочий пример по теме и задает базовый шаблон для собственного кода.
- строки выполняются последовательно: подготовка данных, применение API и получение результата.
- ключевые конструкции этого блока стоит воспринимать как каркас, который дополняется доменной логикой.
- после запуска проверяют, что фактический результат совпадает с ожидаемым сценарием.
- в production-версии дополнительно закрывают граничные случаи и добавляют проверку ошибок.
Тестовая пирамида на практике
Удобная стратегия для реального проекта:
- много unit-тестов (
JUnit 5+Mockito) — быстрый feedback на бизнес-логику; - меньше web-тестов (
MockMvc) — проверка контрактов контроллеров; - немного интеграционных тестов (
@SpringBootTest, Testcontainers) — проверка "склейки" с БД и инфраструктурой.
Если пирамида перевёрнута (почти всё на @SpringBootTest), сборка становится медленной и команда начинает реже запускать тесты локально.
Тестирование конкурентного кода
Для ExecutorService, CompletableFuture и логики с общим изменяемым состоянием нужны отдельные проверки. Иначе ошибки синхронизации часто проявляются только под нагрузкой.
Минимальный набор:
- Тест на таймаут (
orTimeout,get(timeout, ...)) для внешних вызовов. - Тест на отмену (
future.cancel(true)) и корректную обработкуInterruptedException. - Повторяемый стресс-кейс (цикл запусков) для поиска race condition.
Практика стабильности:
- избегайте "голых"
Thread.sleep(...)без явного условия; - для ожиданий лучше использовать await-подход и фиксированные дедлайны;
- если тест "плавает", сначала уберите зависимость от реального времени и планировщика ОС.
Расшифровка терминов:
- Race condition — итог зависит от случайного порядка выполнения потоков.
- Flaky test — тест иногда падает без изменений в коде.
- Deadline — максимальное время ожидания, после которого тест завершает шаг с ошибкой.
Связанные главы:
Минимальный план для нового модуля
- На каждый публичный метод сервиса — 2-5 unit-тестов (happy path + edge cases).
- На каждый внешний HTTP-контракт — минимум один
MockMvcтест со статусом и JSON-полями. - На каждый репозиторий или SQL-слой — 1-2 интеграционных теста на реальной БД.
- На критический пользовательский сценарий — один 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 |
Что попробовать
- Тест на глобальный обработчик ошибок —
POSTс пустым телом → 400. @WithMockUserвместе с Security для закрытого эндпоинта.- Testcontainers — один IT на репозиторий.
Дальше
Testcontainers · JWT + Security · Gradle · Spring Boot