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Загрузка примера кода…
Запуск:
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 и логики с общим изменяемым состоянием нужны отдельные проверки. Иначе ошибки синхронизации часто проявляются только под нагрузкой.
Минимальный набор:
- Тест на таймаут (
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