Этап 7 — Тестирование в Spring: как собрать рабочую пирамиду
Тестирование — это способ проверить поведение программы до того, как ошибку увидит пользователь в продакшене. Для новичка полезно запомнить простую формулу: тест это исполняемое доказательство, что код ведет себя ожидаемо в конкретной ситуации. В Java и Spring тесты нужны не только для «поиска багов». Они защищают рефакторинг, ускоряют ревью и делают изменения в существующем коде менее рискованными.
Пирамида важна, потому что каждый уровень отвечает за свой тип риска. Нижние уровни быстрее и дешевле, поэтому там должно быть большинство тестов. Верхние уровни тяжелее и медленнее, поэтому их держат для критичных сквозных сценариев. Если сделать перевернутую пирамиду и покрывать почти всё только верхним уровнем, CI станет медленным, а команда начнет игнорировать нестабильные падения.
Что проверяет каждый уровень пирамиды
Unit-тесты проверяют один класс или маленький фрагмент поведения в изоляции. Они выполняются очень быстро, поэтому именно здесь новичкам удобно закреплять понимание бизнес-правил: расчет скидок, ветвления валидации, правила преобразования данных, проверки граничных значений. Качественный unit-тест проверяет внешнее поведение, а не внутренние детали реализации.
Slice-тесты проверяют один слой Spring-приложения с поддержкой фреймворка, но без поднятия всего контекста. @WebMvcTest проверяет HTTP-контракт: маппинги, валидацию, сериализацию/десериализацию и коды ответов. @DataJpaTest проверяет слой доступа к данным: запросы, маппинг сущностей, транзакционное поведение. Такие тесты медленнее unit-тестов, но заметно быстрее полного интеграционного старта.
Интеграционные тесты проверяют совместную работу нескольких слоев. В Spring это обычно @SpringBootTest, иногда вместе с реальной БД через контейнеры. Эти тесты дорогие по времени, поэтому их лучше оставлять для важных бизнес-потоков: авторизация, оформление заказа, платеж, ключевые переходы статусов.
| Тип теста | Главная цель | Типичные инструменты Spring | Стоимость по времени | Частая ошибка новичка |
|---|---|---|---|---|
| Unit | Проверить чистую логику одного класса | JUnit, Mockito | Очень низкая | Тестировать внутренности фреймворка вместо своей логики |
| Slice | Проверить контракт конкретного слоя | @WebMvcTest, @DataJpaTest | Средняя | Ожидать поведение всего приложения |
| Integration | Проверить реальную связку слоев | @SpringBootTest, Testcontainers | Высокая | Писать слишком много тяжелых тестов для простых случаев |
Что такое Mock и когда он нужен
Mock — это подставной объект, который заменяет реальную зависимость в тесте. Мы подменяем настоящего участника системы контролируемой версией, чтобы изолировать объект проверки. Например, в unit-тесте UserService можно заменить UserRepository на mock и проверить бизнес-решения сервиса без настоящей базы данных.
Mock полезен, когда зависимость не является целью текущего теста, либо когда она медленная, нестабильная или инфраструктурно тяжелая. Не нужно мокировать всё подряд. Если вы проверяете SQL-поведение или JPA-маппинг, тогда нужен реальный слой данных, а не заранее запрограммированный ответ mock-объекта.
Практический пример для начинающей команды Spring
Представим, что вы добавляете регистрацию пользователя. Есть правила: email должен быть уникальным, пароль не короче 8 символов, а при ошибке API обязан вернуть структурированный JSON с понятным сообщением.
Начните с unit-тестов для RegistrationService. Замокайте репозиторий и проверьте решения бизнес-логики: короткий пароль отклоняется, дубликат email отклоняется, корректные данные проходят. Этот слой даёт самую быструю обратную связь во время разработки.
Дальше добавьте @WebMvcTest для RegistrationController. Здесь проверяется HTTP-контракт: на невалидные поля приходит 400, структура JSON-ошибки стабильна, успешный запрос возвращает ожидаемый статус и формат ответа. Такой тест защищает интеграцию с frontend или внешними клиентами API.
Затем добавьте @DataJpaTest для методов репозитория, например existsByEmail. На этом уровне вы проверяете, что запросы и маппинг сущностей действительно работают с базой так, как задумано.
И только после этого оставьте несколько @SpringBootTest сценариев для сквозной проверки: реальный HTTP-запрос, выполнение всей цепочки, сохранение данных и корректный итоговый ответ. Если в проде PostgreSQL, для таких сценариев лучше использовать Testcontainers, чтобы поймать диалектные отличия до релиза.
Нюансы, которые чаще всего путают новичков
Первая ошибка — думать, что больше тестов автоматически означает лучшее качество. На практике важно качество сигнала: лучше меньше, но точных тестов, которые падают только при реальной регрессии.
Вторая ошибка — смешивать границы слоев. Например, проверять HTTP-коды в unit-тесте сервиса или сложные бизнес-ветки в @WebMvcTest. Держите ассерты в пределах ответственности конкретного уровня.
Третья ошибка — flaky-тесты. Обычно причина в общем изменяемом состоянии, зависимостях от времени, случайных данных без контроля или неявной зависимости от порядка запуска. Лечится это детерминированными данными, изолированными фикстурами и явным setup/teardown.
Если тест проходит на H2, но падает на PostgreSQL, это не случайность. Это признак того, что тестовое окружение слишком далеко от реального. Для критичных запросов и транзакций обязательно нужна проверка с реальной СУБД.
Итог этапа
Тестирование в Spring — это стратегия управления рисками, а не выбор одной «правильной» аннотации. Рабочая пирамида выглядит так: много быстрых unit-тестов, достаточное количество slice-тестов для контрактов слоев и небольшой набор самых ценных интеграционных сценариев. Mock используйте осознанно для изоляции поведения, а не механически.