Spring Test: контекст и слайсы
Проверял контроллер двумя способами. Через MockMvc с одним поднятым контроллером запрос прошёл всю цепочку веб-слоя - маршрутизацию, разбор тела, проверку данных, превращение ответа в JSON - и вернул честный код: 400 на кривом теле, 200 на правильном, 405 на чужом методе. Настоящий сервер при этом не запускался, порт не занимался.
Отсюда главный выбор в тестах Spring: сколько приложения поднимать. Целиком - медленно, зато по-настоящему; срезом - быстро, но проверяется только слой.
// Формулировки: «чем @Mock отличается от @MockBean?», «что такое срез контекста?», «почему тесты Spring медленные и как это лечить?», «в чём подвох @DataJpaTest?».
Полный контекст и срезы
Полный контекст - это запущенное приложение целиком: база, очереди и все бины, то есть объекты, которыми управляет Spring. Так проверяют сценарий целиком, но старт стоит секунд, а зависимостей нужно много.
Срез поднимает только нужный слой. Веб-срез создаёт контроллеры, преобразователи JSON и обработчики ошибок, но не создаёт сервисы и репозитории - их подставляют моками. Срез для слоя данных наоборот: репозитории и база есть, веб-слоя нет.
// Ключевая вещь про скорость: контексты кэшируются между тестовыми классами по НАБОРУ настроек. Пока конфигурация совпадает, второй класс переиспользует уже поднятый контекст. Стоит одному классу добавить свою мелочь - и поднимается ещё один контекст. Поэтому конфигурации в тестах держат единообразными, а не подкручивают в каждом классе: именно россыпь мелких отличий превращает прогон в многоминутный.
- срез контекста
- поднимается только нужный слой, остальное подставляется моками
- кэш контекстов
- контекст переиспользуется, пока набор настроек совпадает
Мок в тесте и мок в контексте
Разница между обычным моком и моком-бином принципиальная. Обычный мок - просто объект, который ты сам передаёшь в конструктор проверяемого класса. Контейнер о нём не знает, и это правильный инструмент для быстрого теста без Spring вообще.
Мок-бин ПОДМЕНЯЕТ настоящий бин в поднятом контексте. Он нужен, когда контекст всё-таки поднимается и надо заменить одну зависимость - например, клиент внешнего сервиса. Правило выбора простое: контекст не поднимается - обычный мок; поднимается - мок-бин, иначе подмены не произойдёт, и в дело пойдёт настоящий бин.
// У мок-бина есть цена, о которой забывают: он ИЗМЕНЯЕТ набор настроек контекста, а значит создаёт отдельный кэшированный контекст. Десять классов с разными наборами подменённых бинов - десять поднятий приложения за прогон.
- обычный мок
- объект, который ты сам передаёшь в конструктор, без контейнера
- мок-бин
- подменяет бин в поднятом контексте, но добавляет свой вариант контекста
Откат транзакции и что он прячет
Тесты слоя данных по умолчанию идут в транзакции, которая откатывается после теста. Это удобно: база остаётся чистой, тесты не мешают друг другу, порядок не важен.
Но откат создаёт условия, которых в проде нет. Внутри одной транзакции живёт одна сессия работы с базой, поэтому загруженные объекты всё время остаются связанными с ней и их ленивые связи подгружаются свободно. Проблемы с ленивой загрузкой, которые в реальном приложении вылезают после закрытия сессии, в таком тесте не проявятся вообще.
// Второе, что прячет откат: ошибки, возникающие только при настоящей записи. Пока изменения не вытолкнуты в базу, нарушение уникального индекса или внешнего ключа не обнаружится. Лечится принудительным выталкиванием изменений в нужных местах теста или отдельным тестом без отката - тогда и уборку данных придётся делать самому.
- откат после теста
- база чистая, но условия отличаются от продовых
Как отвечать: «Чем обычный мок отличается от мок-бина?»
Областью действия. Обычный мок это просто объект: я создаю его и сам передаю в конструктор проверяемого класса. Контейнер Spring о нём ничего не знает, поэтому он годится для быстрых тестов без поднятия контекста - и такие тесты я предпочитаю, они идут миллисекунды. Мок-бин работает в поднятом контексте: он подменяет настоящий бин, и все, кто эту зависимость получает, получают подменённую. Нужен он там, где контекст всё равно поднимается и надо отрезать одну зависимость, чаще всего клиент внешнего сервиса. Правило простое: контекст не поднимаю - обычный мок, поднимаю - мок-бин, иначе подмены не случится. И помню про цену: мок-бин меняет набор настроек контекста, а контексты кэшируются именно по нему, так что каждый новый набор подмен это ещё одно поднятие приложения за прогон.
Почему это сильный ответ: различие проведено по области действия, дано правило выбора и назван неочевидный побочный эффект на время прогона.
На чём валят
- −Поднимать полный контекст там, где хватило бы обычного объекта с моками. Секунды против миллисекунд на каждый тест.
- −Обычный мок вместо мок-бина при поднятом контексте. Подмены не происходит, в дело идёт настоящий бин.
- −Свой набор подменённых бинов в каждом классе. Каждый набор это отдельный контекст и ещё одно поднятие приложения.
- −Верить тесту с откатом в вопросах ленивой загрузки. Внутри транзакции проблема не воспроизводится в принципе.
- −Проверять веб-слой поднятием настоящего сервера. Тот же результат даёт MockMvc, только без порта и ожидания старта.
Проверьте себя
Пять вопросов из банка по этой подтеме. Всего их 14, остальные разбираются в тренажёре.
- Что позволяет делать MockMvc?A)Вызывать контроллеры и проверять ответ без реального сетевого сервераB)Поднимать реальный встроенный HTTP-сервер на случайном порту для тестаC)Мокать произвольный бин приложения прямо в контексте SpringD)Подменять базу данных на встроенную H2 в памяти
показать ответ и разбор
+A)Вызывать контроллеры и проверять ответ без реального сетевого сервера// разбор: MockMvc прогоняет запрос через Spring MVC (маппинги, фильтры, сериализацию), но БЕЗ старта реального сервера и сети — быстро. perform(get("/x")).andExpect(status().isOk()) проверяет статус, тело, jsonPath. Мок бинов — это @MockBean, реальный сервер — RANDOM_PORT; это другие механизмы.
- В @WebMvcTest сервис-зависимость контроллера объявили как @Mock. Тест падает — контекст не поднялся. Почему?A)@Mock работает, но требует ещё и @ExtendWith(MockitoExtension)B)@Mock не регистрирует бин в контексте Spring — нужен @MockBeanC)В @WebMvcTest подменять сервисы не разрешеноD)Нужно было объявить сервис как @Autowired, а не мок
показать ответ и разбор
+B)@Mock не регистрирует бин в контексте Spring — нужен @MockBean// разбор: @Mock создаёт объект Mockito, но Spring о нём не знает — в контексте бина нет, и контроллеру некого внедрить. @MockBean кладёт мок именно В контекст, заменяя/добавляя бин, поэтому в Spring-тестах подменяют им. @Mock уместен в чистом юните без контекста. @Autowired реального сервиса в @WebMvcTest не найдёт.
- @DataJpaTest прогнал вставку, но после теста строки в БД нет. Это баг теста?A)Да, забыли вызвать repository.flush() для сохраненияB)Да, нужно снять аннотацию @Transactional с тестового метода, чтобы данные оселиC)Нет, @DataJpaTest по умолчанию откатывает транзакцию после каждого тестаD)Нет, данные ушли в другую схему из-за неверного диалекта
показать ответ и разбор
+C)Нет, @DataJpaTest по умолчанию откатывает транзакцию после каждого теста// разбор: @DataJpaTest оборачивает каждый тест в транзакцию и откатывает её в конце — это фича: тесты не грязнят БД и не зависят друг от друга. Так и задумано, не баг. Побочный эффект: без явного flush отложенные INSERT/constraint-проверки могут не сработать внутри теста — иногда flush нужен, чтобы увидеть нарушение.
- Сюита из 200 @SpringBootTest ползёт минутами. Первый шаг к ускорению?A)Заменить JUnit 5 на JUnit 4 ради скоростиB)Удвоить память JVM у тестового раннераC)Пометить все тесты @Tag и гонять их режеD)Перевести подходящие тесты на слайсы (@WebMvcTest, @DataJpaTest)
показать ответ и разбор
+D)Перевести подходящие тесты на слайсы (@WebMvcTest, @DataJpaTest)// разбор: @SpringBootTest поднимает полный контекст — главный источник тормозов. Там, где нужен один слой, слайс грузит только его и переиспользует кэш контекста — прогон падает в разы. Память лечит симптом, теги лишь прячут медленные тесты, версия JUnit на время старта контекста не влияет. Правильный шаг — сузить область теста.
- Нужен end-to-end тест REST-эндпоинта через реальный HTTP. Какая связка?A)@SpringBootTest(webEnvironment=RANDOM_PORT) плюс TestRestTemplate/WebTestClientB)@DataJpaTest плюс TestEntityManagerC)@WebMvcTest плюс MockMvcD)Обычный @Test с ручным new контроллера
показать ответ и разбор
+A)@SpringBootTest(webEnvironment=RANDOM_PORT) плюс TestRestTemplate/WebTestClient// разбор: Полный стек по реальному HTTP даёт @SpringBootTest с webEnvironment=RANDOM_PORT: приложение стартует на случайном порту, а запросы шлёт TestRestTemplate или WebTestClient — проходит вся цепочка (фильтры, сериализация, безопасность). MockMvc не использует сеть, @DataJpaTest — только JPA, ручной new минует контекст целиком.
дальше
Теорию прочитали. Навык ставится повторением
В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.