1. Full-context web‑тест без сервера
Если вы уже умеете писать @WebMvcTest и умеете поднимать @SpringBootTest, логичный вопрос звучит так: «Зачем изобретать ещё один режим?». Ответ очень приземлённый: иногда приложение ломается не внутри контроллера, не внутри сервиса и не внутри репозитория, а на стыке — там, где всё по отдельности “вроде бы работает”, но вместе внезапно получается 500 и грустный разработчик.
Из режимов @SpringBootTest здесь нужен именно MOCK: slices уже слишком узкие для таких поломок, а реальный сетевой round-trip нам пока не нужен. То есть мы берём полный ApplicationContext, но всё ещё остаёмся внутри JVM.
Представьте себе ContentHub как конвейер: запрос приходит в контроллер, дальше идёт в сервис, дальше в репозиторий, потом назад, и где-то по дороге ещё включаются валидация, сериализация, @ControllerAdvice, фильтры и (если включено) security‑цепочка. В @WebMvcTest мы проверяем только начало конвейера и очень аккуратно имитируем то, что дальше. В unit‑тесте мы проверяем кусочек конвейера без ленты и моторчика — «руками покрутили шестерёнку, убедились, что она крутится».
А вот связка @SpringBootTest + @AutoConfigureMockMvc — это режим «конвейер собран полностью, мотор подключён, но мы гоняем его в тестовом цеху без настоящей доставки по городу». То есть сеть не поднимаем, порты не слушаем, но внутри JVM прогоняем запрос через реальную сборку приложения. Это идеально, когда вы хотите проверить именно сборку слоёв: wiring, конфигурацию, ошибки, JSON‑контракт на стыке и работу с реальными данными.
2. @SpringBootTest и режим MOCK
Пока мы не написали ни одной строчки MockMvc, полезно остановиться и честно проговорить: «А что означает “полный контекст”?». @SpringBootTest — это не просто “аннотация, которая запускает Spring”. Она поднимает практически всё приложение так, как оно поднимется при реальном старте: auto‑configuration, ваши @Configuration, сервисы, репозитории, настройки Jackson, Bean Validation, @ControllerAdvice и так далее.
У @SpringBootTest есть режимы запуска web‑окружения (webEnvironment): NONE, MOCK, RANDOM_PORT, DEFINED_PORT. Здесь нам нужен MOCK, потому что он поднимает web‑часть без настоящего сетевого сервера.
Если сказать максимально честно, MOCK — это «Spring MVC внутри теста», а не «Tomcat/Jetty/Netty наружу». Запрос будет представлен объектом MockHttpServletRequest, ответ — MockHttpServletResponse. Никакого TCP, никакого порта, никакого “а у меня firewall”. Только вы, Spring MVC и ваша логика.
Мини‑каркас, чтобы зафиксировать мысль в коде:
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
class ContentHubMockWebEnvironmentTest {
// Важно: поднимется полный ApplicationContext приложения.
// И дополнительно — mock web environment (без реального сетевого сервера и портов).
}
И ещё один практический момент: «полный контекст» означает, что если у вас в приложении есть база данных, миграции и репозитории, они будут реально участвовать. В отличие от @WebMvcTest, где data‑слой обычно вообще не поднимается. Поэтому в full‑context тестах тема предсказуемых данных и test profile перестаёт быть «красивой архитектурой» и становится вопросом выживания.
@AutoConfigureMockMvc и MockMvc
Теперь подведём к главному герою дня: MockMvc. Сама по себе аннотация @SpringBootTest не обязана дать вам MockMvc как готовый инструмент. Она поднимает контекст, но не обещает, что вы сразу получите удобный “HTTP‑пульт управления”. Именно @AutoConfigureMockMvc говорит Spring Boot: «Раз уж это web‑приложение, сконфигурируй MockMvc и положи его в контекст, чтобы я мог его заинжектить».
Психологически это важный шаг. Мы как будто говорим: «Я хочу интеграционный тест, но хочу взаимодействовать с ним на уровне HTTP‑границы». И тогда у нас получается красивый компромисс: тест запускается как интеграционный (реальные бины, реальная сборка слоёв), но пишется как MVC‑тест (URI, status, headers, JSON). Для junior‑разработчика это очень комфортно, потому что синтаксис знакомый ещё со времён @WebMvcTest.
Минимальный каркас класса выглядит так:
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest // Поднимаем полный контекст приложения.
@AutoConfigureMockMvc // Просим Spring Boot сконфигурировать MockMvc и положить его в контекст.
class PublicArticleFullContextTest {
}
А вот так мы забираем MockMvc из контекста:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.web.servlet.MockMvc;
class PublicArticleFullContextTest {
@Autowired
MockMvc mockMvc; // "HTTP‑пульт": выполняет запросы через Spring MVC без поднятия сервера и сети.
}
Если у вас в проекте подключён AssertJ-friendly путь, то рядом может жить и MockMvcTester. Его приятно использовать, когда вы хотите писать проверки в более “fluent” стиле, но по смыслу это тот же HTTP‑пульт.
3. Каркас full-context теста
Сейчас будет самый важный (и самый простой) момент лекции: давайте сделаем тест, который действительно выполняет HTTP‑запрос, но делает это в полном контексте и без реального сервера. Здесь легко попасть в ловушку «я понял теорию, но код писать страшно». Не страшно: синтаксис почти такой же, как в @WebMvcTest, просто под капотом теперь другой масштаб.
Для примера возьмём публичный endpoint ContentHub: чтение статьи по slug. В идеальном мире у нас уже есть миграции, тестовый профиль, а тестовые данные можно заранее подложить. Чтобы не усложнять, пока сделаем запрос на “несуществующий slug” и проверим, что возвращается корректная ошибка в формате ApiProblem (или эквивалентного error contract). Это удобный старт, потому что не требует заранее загружать статьи в БД — мы проверяем негативный сценарий, который должен работать всегда.
Каркас теста:
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest // Полный контекст приложения (сервисный и data-слой тоже поднимутся).
@AutoConfigureMockMvc // Добавляем MockMvc в контекст, чтобы тест мог выполнять HTTP-запросы.
class PublicArticleFullContextTest {
}
Теперь добавим один тест‑метод. Обратите внимание: здесь мы уже “говорим HTTP”, но у нас полный контекст, то есть ошибки должны проходить через реальный @ControllerAdvice, сериализоваться реальным ObjectMapper, и всё это вместе должно дать стабильный JSON‑ответ.
import org.junit.jupiter.api.Test;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@Test
void returns404ForMissingSlug() throws Exception {
// Выполняем запрос так, как это сделал бы клиент: GET по URI ресурса.
mockMvc.perform(get("/api/public/articles/missing-slug"))
// Проверяем только HTTP-статус: 404 — ресурс не найден.
.andExpect(status().isNotFound());
}
И если мы хотим проверить не просто статус, а ещё и контракт ошибки, добавим jsonPath. В этом моменте как раз становится видно преимущество режима: вы проверяете всю цепочку “ресурс не найден” → “выбросили доменное исключение” → “перевели его в ApiProblem” → “отдали JSON”.
import org.junit.jupiter.api.Test;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@Test
void returnsProblemDetailsForMissingSlug() throws Exception {
mockMvc.perform(get("/api/public/articles/missing-slug"))
.andExpect(status().isNotFound())
// Проверяем, что error contract действительно отдал ожидаемый код ошибки.
.andExpect(jsonPath("$.errorCode").value("ARTICLE_NOT_FOUND"));
}
Если вы уже привыкли писать accept(MediaType.APPLICATION_JSON), это тоже можно и нужно делать, особенно когда вы фиксируете контракт API. В full‑context режиме это дополнительно защищает вас от странных “по умолчанию” поведения.
import org.junit.jupiter.api.Test;
import org.springframework.http.MediaType;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@Test
void returnsJsonForPublicEndpoint() throws Exception {
mockMvc.perform(get("/api/public/articles/missing-slug")
// Явно говорим: "я ожидаю JSON", чтобы зафиксировать контракт.
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isNotFound());
}
И последнее маленькое, но важное замечание: поскольку это полный контекст, вы можете (при необходимости) заинжектить не только MockMvc, но и, например, репозиторий, чтобы проверить состояние данных. Это уже ближе к «дорогим» тестам с высоким ROI — мы здесь только обозначаем возможность, чтобы вы не думали, что full‑context тест — это “просто ещё один MVC‑тест”.
4. Отличия от @WebMvcTest
Очень легко сделать неправильный вывод: «Ну окей, я как писал MockMvc, так и пишу. Значит, это то же самое, просто аннотации другие». Увы (или к счастью), нет. Главное отличие не в синтаксисе, а в том, какой мир находится за вашим запросом и что означает зелёный тест. В @WebMvcTest мир маленький и специально “обрезанный”. В @SpringBootTest + @AutoConfigureMockMvc мир настоящий — насколько это возможно без сети.
Чтобы зафиксировать различия без философии, удобно сравнить режимы таблицей:
| Характеристика | @WebMvcTest | @SpringBootTest + @AutoConfigureMockMvc |
|---|---|---|
| Какой контекст поднимается | Узкий MVC‑slice (обычно 1 контроллер + MVC инфраструктура) | Полный ApplicationContext приложения |
| Типичные зависимости контроллера | Часто мокируются (@MockitoBean/@MockBean) | Обычно реальные сервисы/репозитории/конфиги |
| База данных | Обычно не участвует | Может участвовать (в зависимости от конфигурации) |
| Цена запуска | Низкая / средняя | Высокая |
| Что особенно хорошо ловит | Ошибки HTTP‑контракта контроллера | Поломки связки слоёв и конфигурации, ошибки на стыке |
| Чего не хочет делать | Проверять бизнес‑логику и persistence | Дублировать то, что уже закрыто unit и slices |
Это различие помогает не наступать на очень популярные грабли. Если вы переносите все ваши @WebMvcTest в full‑context режим, вы почти гарантированно получите медленный suite и минимальную дополнительную ценность. Если вы, наоборот, пытаетесь поломку связки слоёв ловить только @WebMvcTest‑ом, вы будете моками “додумывать реальность” — и в какой-то момент тест станет зелёным, а прод будет красным (обычно в пятницу вечером, потому что у багов есть чувство юмора).
Ещё одно важное следствие: в full‑context тесте вы обычно меньше мокаете и больше управляете данными и конфигурацией. И это логично: если цель — проверить настоящую сборку слоёв, то подменять половину слоёв на моки — это как проверять прочность моста, заменив половину пролётов картонкой. Технически вы “что-то проверили”, но уверенность в результате такая себе.
5. Граница доказательства теста
Здесь важно сразу поставить границу. Full-context MockMvc отлично доказывает, что запрос проходит через реальную MVC-цепочку внутри полного ApplicationContext: контроллер, сервисы, репозитории, @ControllerAdvice, Jackson, filters и security. Именно за этот класс поломок мы и платим ценой полного контекста.
Но реального сетевого round-trip тут нет: нет порта, TCP-соединения и настоящего HTTP-клиента. Поэтому зелёный тест означает «приложение корректно собрано внутри JVM», а не «мы уже проверили поведение живого сервера». Для этого режима этого знания достаточно: сейчас нам важна именно внутренняя сборка приложения.
6. Типичные ошибки в full-context тестах
В этом месте обычно хочется сказать “ну всё понятно, поехали писать тесты пачками”. И вот тут как раз полезно немного притормозить и посмотреть на ошибки, которые чаще всего делают разработчики, впервые перейдя к full‑context MockMvc. Они не сложные, они коварные: вы не заметите их, пока suite не разрастётся и не начнёт вести себя как капризный кот — «сегодня запускаюсь, завтра нет, почему — не скажу».
Ошибка №1: забыть @AutoConfigureMockMvc и удивляться, что MockMvc не инжектится.
@SpringBootTest поднимает контекст, но не обязан автоматически давать MockMvc. В результате вы получаете NoSuchBeanDefinitionException или просто не можете заинжектить инструмент. Лечится просто: для full‑context web‑тестов “две аннотации” — это не украшение, а минимальный комплект.
Ошибка №2: считать, что раз тест “HTTP‑шный”, то это почти как реальный сервер.
Интуитивно кажется: “я же делаю GET/POST, значит это почти прод”. На деле нет: это Spring MVC в mock‑окружении. HTTP‑семантика здесь настоящая для Spring MVC, но нет реального порта, клиента и сетевого round-trip. Этот режим отлично ловит сбои внутри приложения, а не транспорт вокруг него.
Ошибка №3: перенести ментальную модель @WebMvcTest и замокать всё подряд.
В full‑context тесте мокирование половины приложения обычно убивает смысл режима. Да, бывают случаи, когда надо подменить внешний адаптер или тяжёлую интеграцию, но это должно быть осознанно и точечно. Если ваш full‑context тест выглядит как @WebMvcTest, только медленнее, — вы платите дорого за то, что могли получить дешево.
Ошибка №4: не активировать тестовый профиль и случайно тестировать “как в проде”.
Эта ошибка особенно болезненна, когда в конфигурации есть реальные адреса, реальные креды или реальные пути к файловой системе. В проекте курса это обычно решается дисциплиной application-test.yml и @ActiveProfiles("test"). Full‑context тесты сильны, но они не должны “внезапно” ходить туда, куда вы не планировали.
Ошибка №5: писать тест, который проверяет только status().isOk().
Полный контекст запускается дорогой ценой. Если вы тратите эту цену ради проверки “ну… 200 же?”, то вы покупаете Ferrari, чтобы доехать до соседнего магазина за хлебом (и ещё ищете парковку 20 минут). В full‑context тестах особенно важно проверять то, за что вы платите: error contract, сериализацию важных полей, реальную связку слоёв, поведение на стыке.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ