сеньорчикОткрыть в Telegram
← вся теориятеория к собесу · Тестирование на Java

Spring Test: контекст и слайсы

Тесты Spring: поднимать всё или кусок

Проверял контроллер двумя способами. Через MockMvc с одним поднятым контроллером запрос прошёл всю цепочку веб-слоя - маршрутизацию, разбор тела, проверку данных, превращение ответа в JSON - и вернул честный код: 400 на кривом теле, 200 на правильном, 405 на чужом методе. Настоящий сервер при этом не запускался, порт не занимался.

Отсюда главный выбор в тестах Spring: сколько приложения поднимать. Целиком - медленно, зато по-настоящему; срезом - быстро, но проверяется только слой.

// Формулировки: «чем @Mock отличается от @MockBean?», «что такое срез контекста?», «почему тесты Spring медленные и как это лечить?», «в чём подвох @DataJpaTest?».

Полный контекст и срезы

Полный контекст - это запущенное приложение целиком: база, очереди и все бины, то есть объекты, которыми управляет Spring. Так проверяют сценарий целиком, но старт стоит секунд, а зависимостей нужно много.

Срез поднимает только нужный слой. Веб-срез создаёт контроллеры, преобразователи JSON и обработчики ошибок, но не создаёт сервисы и репозитории - их подставляют моками. Срез для слоя данных наоборот: репозитории и база есть, веб-слоя нет.

// Ключевая вещь про скорость: контексты кэшируются между тестовыми классами по НАБОРУ настроек. Пока конфигурация совпадает, второй класс переиспользует уже поднятый контекст. Стоит одному классу добавить свою мелочь - и поднимается ещё один контекст. Поэтому конфигурации в тестах держат единообразными, а не подкручивают в каждом классе: именно россыпь мелких отличий превращает прогон в многоминутный.

срез контекста
поднимается только нужный слой, остальное подставляется моками
кэш контекстов
контекст переиспользуется, пока набор настроек совпадает

Мок в тесте и мок в контексте

Разница между обычным моком и моком-бином принципиальная. Обычный мок - просто объект, который ты сам передаёшь в конструктор проверяемого класса. Контейнер о нём не знает, и это правильный инструмент для быстрого теста без Spring вообще.

Мок-бин ПОДМЕНЯЕТ настоящий бин в поднятом контексте. Он нужен, когда контекст всё-таки поднимается и надо заменить одну зависимость - например, клиент внешнего сервиса. Правило выбора простое: контекст не поднимается - обычный мок; поднимается - мок-бин, иначе подмены не произойдёт, и в дело пойдёт настоящий бин.

// У мок-бина есть цена, о которой забывают: он ИЗМЕНЯЕТ набор настроек контекста, а значит создаёт отдельный кэшированный контекст. Десять классов с разными наборами подменённых бинов - десять поднятий приложения за прогон.

обычный мок
объект, который ты сам передаёшь в конструктор, без контейнера
мок-бин
подменяет бин в поднятом контексте, но добавляет свой вариант контекста

Откат транзакции и что он прячет

Тесты слоя данных по умолчанию идут в транзакции, которая откатывается после теста. Это удобно: база остаётся чистой, тесты не мешают друг другу, порядок не важен.

Но откат создаёт условия, которых в проде нет. Внутри одной транзакции живёт одна сессия работы с базой, поэтому загруженные объекты всё время остаются связанными с ней и их ленивые связи подгружаются свободно. Проблемы с ленивой загрузкой, которые в реальном приложении вылезают после закрытия сессии, в таком тесте не проявятся вообще.

// Второе, что прячет откат: ошибки, возникающие только при настоящей записи. Пока изменения не вытолкнуты в базу, нарушение уникального индекса или внешнего ключа не обнаружится. Лечится принудительным выталкиванием изменений в нужных местах теста или отдельным тестом без отката - тогда и уборку данных придётся делать самому.

откат после теста
база чистая, но условия отличаются от продовых

Как отвечать: «Чем обычный мок отличается от мок-бина?»

Областью действия. Обычный мок это просто объект: я создаю его и сам передаю в конструктор проверяемого класса. Контейнер Spring о нём ничего не знает, поэтому он годится для быстрых тестов без поднятия контекста - и такие тесты я предпочитаю, они идут миллисекунды. Мок-бин работает в поднятом контексте: он подменяет настоящий бин, и все, кто эту зависимость получает, получают подменённую. Нужен он там, где контекст всё равно поднимается и надо отрезать одну зависимость, чаще всего клиент внешнего сервиса. Правило простое: контекст не поднимаю - обычный мок, поднимаю - мок-бин, иначе подмены не случится. И помню про цену: мок-бин меняет набор настроек контекста, а контексты кэшируются именно по нему, так что каждый новый набор подмен это ещё одно поднятие приложения за прогон.

Почему это сильный ответ: различие проведено по области действия, дано правило выбора и назван неочевидный побочный эффект на время прогона.

На чём валят

  • Поднимать полный контекст там, где хватило бы обычного объекта с моками. Секунды против миллисекунд на каждый тест.
  • Обычный мок вместо мок-бина при поднятом контексте. Подмены не происходит, в дело идёт настоящий бин.
  • Свой набор подменённых бинов в каждом классе. Каждый набор это отдельный контекст и ещё одно поднятие приложения.
  • Верить тесту с откатом в вопросах ленивой загрузки. Внутри транзакции проблема не воспроизводится в принципе.
  • Проверять веб-слой поднятием настоящего сервера. Тот же результат даёт MockMvc, только без порта и ожидания старта.

Проверьте себя

Пять вопросов из банка по этой подтеме. Всего их 14, остальные разбираются в тренажёре.

  1. #spring_testing1 / 5
    Что позволяет делать MockMvc?
    A)Вызывать контроллеры и проверять ответ без реального сетевого сервера
    B)Поднимать реальный встроенный HTTP-сервер на случайном порту для теста
    C)Мокать произвольный бин приложения прямо в контексте Spring
    D)Подменять базу данных на встроенную H2 в памяти
    показать ответ и разбор
    +A)Вызывать контроллеры и проверять ответ без реального сетевого сервера

    // разбор: MockMvc прогоняет запрос через Spring MVC (маппинги, фильтры, сериализацию), но БЕЗ старта реального сервера и сети — быстро. perform(get("/x")).andExpect(status().isOk()) проверяет статус, тело, jsonPath. Мок бинов — это @MockBean, реальный сервер — RANDOM_PORT; это другие механизмы.

  2. #spring_testing2 / 5
    В @WebMvcTest сервис-зависимость контроллера объявили как @Mock. Тест падает — контекст не поднялся. Почему?
    A)@Mock работает, но требует ещё и @ExtendWith(MockitoExtension)
    B)@Mock не регистрирует бин в контексте Spring — нужен @MockBean
    C)В @WebMvcTest подменять сервисы не разрешено
    D)Нужно было объявить сервис как @Autowired, а не мок
    показать ответ и разбор
    +B)@Mock не регистрирует бин в контексте Spring — нужен @MockBean

    // разбор: @Mock создаёт объект Mockito, но Spring о нём не знает — в контексте бина нет, и контроллеру некого внедрить. @MockBean кладёт мок именно В контекст, заменяя/добавляя бин, поэтому в Spring-тестах подменяют им. @Mock уместен в чистом юните без контекста. @Autowired реального сервиса в @WebMvcTest не найдёт.

  3. #spring_testing3 / 5
    @DataJpaTest прогнал вставку, но после теста строки в БД нет. Это баг теста?
    A)Да, забыли вызвать repository.flush() для сохранения
    B)Да, нужно снять аннотацию @Transactional с тестового метода, чтобы данные осели
    C)Нет, @DataJpaTest по умолчанию откатывает транзакцию после каждого теста
    D)Нет, данные ушли в другую схему из-за неверного диалекта
    показать ответ и разбор
    +C)Нет, @DataJpaTest по умолчанию откатывает транзакцию после каждого теста

    // разбор: @DataJpaTest оборачивает каждый тест в транзакцию и откатывает её в конце — это фича: тесты не грязнят БД и не зависят друг от друга. Так и задумано, не баг. Побочный эффект: без явного flush отложенные INSERT/constraint-проверки могут не сработать внутри теста — иногда flush нужен, чтобы увидеть нарушение.

  4. #spring_testing4 / 5
    Сюита из 200 @SpringBootTest ползёт минутами. Первый шаг к ускорению?
    A)Заменить JUnit 5 на JUnit 4 ради скорости
    B)Удвоить память JVM у тестового раннера
    C)Пометить все тесты @Tag и гонять их реже
    D)Перевести подходящие тесты на слайсы (@WebMvcTest, @DataJpaTest)
    показать ответ и разбор
    +D)Перевести подходящие тесты на слайсы (@WebMvcTest, @DataJpaTest)

    // разбор: @SpringBootTest поднимает полный контекст — главный источник тормозов. Там, где нужен один слой, слайс грузит только его и переиспользует кэш контекста — прогон падает в разы. Память лечит симптом, теги лишь прячут медленные тесты, версия JUnit на время старта контекста не влияет. Правильный шаг — сузить область теста.

  5. #spring_testing5 / 5
    Нужен end-to-end тест REST-эндпоинта через реальный HTTP. Какая связка?
    A)@SpringBootTest(webEnvironment=RANDOM_PORT) плюс TestRestTemplate/WebTestClient
    B)@DataJpaTest плюс TestEntityManager
    C)@WebMvcTest плюс MockMvc
    D)Обычный @Test с ручным new контроллера
    показать ответ и разбор
    +A)@SpringBootTest(webEnvironment=RANDOM_PORT) плюс TestRestTemplate/WebTestClient

    // разбор: Полный стек по реальному HTTP даёт @SpringBootTest с webEnvironment=RANDOM_PORT: приложение стартует на случайном порту, а запросы шлёт TestRestTemplate или WebTestClient — проходит вся цепочка (фильтры, сериализация, безопасность). MockMvc не использует сеть, @DataJpaTest — только JPA, ручной new минует контекст целиком.

дальше

Теорию прочитали. Навык ставится повторением

В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.