1. Смена режима: меняется смысл, не API
Полный контекст уже поднят, а реального сервера всё ещё нет. Теперь вопрос не в том, зачем нужен MOCK, а в том, каким API писать HTTP-проверки так, чтобы тесты оставались читаемыми.
Полезно держать очень простую мысль: MockMvc не “про web-slice” и не “про integration”. Это просто вход через HTTP в Spring MVC.
А вот какой контекст стоит за этим входом, решает всё остальное. В @WebMvcTest за ним узкий MVC-срез. В @SpringBootTest + @AutoConfigureMockMvc за тем же вызовом get(...) уже стоят реальные сервисы, репозитории, конфигурация, filters и @ControllerAdvice. Дальше важен именно выбор стиля записи: raw MockMvc или MockMvcTester.
В full-context режиме особенно заметно, что сам HTTP-вход не меняется: меняется не формулировка запроса, а глубина того, что стоит за ним. Один и тот же GET-запрос может дойти либо до узкого среза из контроллера и пары моков, либо до настоящего сервиса, репозитория и всей конфигурации приложения. Поэтому в этой теме мы говорим не столько о “другом способе тестировать”, сколько о другом смысле того же самого HTTP-API.
2. Два входа: MockMvc и MockMvcTester
Когда вы работаете в full-context режиме, вы можете продолжать писать тесты на “сыром” MockMvc, как делали это раньше, или использовать MockMvcTester, чтобы держаться AssertJ-стиля и писать более компактные проверки. Самое важное здесь — не пытаться «переписать всё под новый стиль ради моды», а выбрать инструмент под задачу. В интеграционном тесте цена ошибки выше: если код теста становится нечитаемым, чинить его будет больнее, чем в быстрых slices.
В full-context режиме у вас обычно будет один и тот же фундамент:
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
@AutoConfigureMockMvc // Включает регистрацию MockMvc в полном контексте
class PublicArticleFullContextTest {
// Здесь можно инжектить MockMvc или MockMvcTester
}
А дальше вы выбираете: что именно вы инжектите в тестовый класс. MockMvc даёт вам максимально явный “низкоуровневый” стиль с perform(...) и цепочкой andExpect(...). MockMvcTester даёт более “читабельный” AssertJ-ритм, где результат удобно проверять как объект.
Чтобы разница не казалась мистикой, вот аккуратная табличка-подсказка — не догма, а ориентир:
| Инструмент | Как выглядит стиль | Когда особенно удобен |
|---|---|---|
| MockMvc | perform(...) → andExpect(...) | Когда важна явность HTTP-деталей и вы хотите видеть каждую проверку рядом с запросом |
| MockMvcTester | exchange() → assertThat(result)... | Когда вы хотите единый AssertJ-стиль и более компактный код, особенно в full-context наборах |
В этом курсе мы не воюем за “единственно правильный” API. Мы воюем за то, чтобы тест читался как сценарий: какой запрос, какой ответ, почему это важно.
3. Raw MockMvc: видим контракт целиком
Когда вы пишете full-context тесты, очень хочется «спрятать всё лишнее», потому что и так контекст большой, тест медленнее, а вам хочется, чтобы хотя бы код был коротким. Но есть подвох: именно в интеграционных тестах важно не скрывать ключевые детали HTTP-контракта. Поэтому сырой MockMvc часто остаётся самым честным вариантом: вы видите URI, заголовки, статус и смысловые проверки тела ответа в одном месте, без дополнительного слоя абстракций.
Во всех happy-path примерах ниже подразумевается один и тот же предсказуемый dataset: опубликованная статья уже загружена, например через @Sql("/sql/public/published-article.sql"). Без такого стартового состояния полный контекст честно вернёт пустой результат или 404, и это будет уже другая история.
Начнём с максимально прямого примера: читаем карточку опубликованной статьи по slug через публичный endpoint. Сценарий сам по себе простой, но как “носитель техники” — отличный.
Мини-каркас теста — полный контекст, MockMvc инжектится Spring’ом:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.web.servlet.MockMvc;
class PublicArticleFullContextTest {
@Autowired
MockMvc mockMvc; // Главный вход для имитации HTTP-запросов в Spring MVC
}
А теперь — сам тест. Здесь важно, что запрос выглядит точно так же, как в @WebMvcTest, но «под капотом» работает полный контекст приложения:
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.*;
class PublicArticleFullContextTest {
@Test
void readsPublicArticleDetails() throws Exception {
mockMvc.perform(
// Делаем HTTP GET на публичный endpoint
get("/api/public/articles/spring-basics")
// Явно задаём, что хотим получить JSON в ответе
.accept(MediaType.APPLICATION_JSON)
)
// Проверяем базовый контракт: статус 200
.andExpect(status().isOk())
// Проверяем смысловой фрагмент JSON, чтобы тест не свёлся к «просто 200»
.andExpect(jsonPath("$.slug").value("spring-basics"));
}
}
Обратите внимание на две детали.
Первая — accept(MediaType.APPLICATION_JSON). В controller-тестах мы уже приучали себя не надеяться на «дефолты». В full-context режиме это ещё важнее, потому что теперь в цепочке могут участвовать дополнительные настройки content negotiation, converters и даже фильтры.
Вторая — jsonPath("$.slug"). Даже если мы не проверяем весь payload целиком, одна-две смысловые проверки помогают не скатиться в тест «просто вернул 200».
Если хочется показать, что параметры запроса и метаданные ответа тоже остаются тем же контрактом, можно проверить публичный список статей с пагинацией — без углубления в data-layer логику:
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.*;
class PublicArticleFullContextTest {
@Test
void readsPublicArticlePage() throws Exception {
mockMvc.perform(
// Параметры page/size — часть HTTP-контракта
get("/api/public/articles?page=0&size=10")
.accept(MediaType.APPLICATION_JSON)
)
.andExpect(status().isOk())
// Минимальная проверка формы ответа: items должен быть массивом
.andExpect(jsonPath("$.items").isArray());
}
}
Да, это похоже на slice-тест. Но смысл теперь другой: мы не доказываем «контроллер умеет вернуть JSON», а проверяем, что в полном приложении endpoint реально отдаёт корректный JSON-формат страницы, используя реальные настройки сериализации и настоящие зависимости без вручную выстроенных моков на всё.
4. MockMvcTester: AssertJ-стиль
После нескольких raw MockMvc тестов у новичка обычно появляется желание: «Можно сделать так же, но покороче?». И вот тут MockMvcTester становится очень приятным инструментом. Он не меняет природу теста и не делает его “менее интеграционным”. Он просто позволяет писать проверки чуть более декларативно, особенно если вы уже привыкли к AssertJ как к основному языку утверждений.
Главная идея MockMvcTester — вы выполняете запрос, получаете результат в виде объекта, а затем проверяете его AssertJ-стилем.
Пример того же публичного чтения статьи, но через MockMvcTester:
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.assertj.MockMvcTester;
class PublicArticleFullContextTesterTest {
@Autowired
MockMvcTester mvc; // Упрощённый вход для HTTP-запросов в AssertJ-стиле
@Test
void readsPublicArticleDetails() {
var result = mvc.get()
// Явно задаём URI, который тестируем
.uri("/api/public/articles/spring-basics")
// Как и в raw MockMvc: фиксируем, что ожидаем JSON
.accept(MediaType.APPLICATION_JSON)
.exchange();
// Базовая проверка HTTP-контракта
Assertions.assertThat(result).hasStatusOk();
}
}
Тут есть один нюанс: чтобы пример был честным и полезным, нам нужно показать и проверку тела. Дальше нас интересует не строка JSON целиком, а конкретный смысловой путь внутри ответа.
Например, в псевдо-реалистичном виде это может выглядеть так:
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.assertj.MockMvcTester;
class PublicArticleFullContextTesterTest {
@Autowired
MockMvcTester mvc; // Инжектится Spring'ом при full-context конфигурации
@Test
void returnsSlugInJson() {
var result = mvc.get()
.uri("/api/public/articles/spring-basics")
.accept(MediaType.APPLICATION_JSON)
.exchange();
Assertions.assertThat(result).hasStatusOk();
// Проверяем конкретный JSON-путь, чтобы тест оставался «про контракт», а не «про строки»
Assertions.assertThat(result).bodyJson()
.extractingPath("$.slug").isEqualTo("spring-basics");
}
}
Что важно педагогически: вы не “променяли” HTTP-понимание на магию. URI, Accept, точка проверки "$.slug" — всё равно читаются глазами. Просто проверки сгруппированы чуть иначе: сначала вы видите запрос, потом — набор осмысленных утверждений.
Есть и практическая дисциплина: если вы используете MockMvcTester, старайтесь держать в одном тестовом классе один доминирующий стиль. Иначе тесты превращаются в разговор двух разных диалектов в одном предложении: вроде бы и всё понятно, но мозг начинает уставать быстрее.
5. Helpers из web-slice: аккуратно
Если вы уже набили руку на @WebMvcTest, у вас почти наверняка появились маленькие helpers: что-то вроде getJson(...), postJson(...), readFixture(...). И это хорошо: повторяющийся код действительно надо сокращать. Но в full-context тестах есть тонкий риск: вы начинаете “оборачивать” всё подряд, потому что тесты и так дорогие, и хочется «спрятать побольше». Итог — тест становится коротким, но перестаёт объяснять, что именно происходит по HTTP.
Хороший helper в интеграционном тесте — это обычно 3–5 строк, которые убирают шум, но не прячут смысл.
Классический пример, который отлично переносится из web-slice в full-context:
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.ResultActions;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
class PublicArticleFullContextTest {
private ResultActions getJson(String uri) throws Exception {
// Helper стандартизирует Accept: application/json и не скрывает сам URI
return mockMvc.perform(get(uri).accept(MediaType.APPLICATION_JSON));
}
}
Такой helper делает две вещи: он стандартизирует Accept: application/json и убирает повторение. Но он не прячет URI, а значит тест по-прежнему читается как «вот эндпоинт, вот ожидание».
Дальше вы можете использовать его в тесте, не превращая код в DSL:
import org.junit.jupiter.api.Test;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
class PublicArticleFullContextTest {
@Test
void readsPublicArticleDetails() throws Exception {
// В тесте сразу видно, какой endpoint вызываем
getJson("/api/public/articles/spring-basics")
// И сразу видно ключевые ожидания по HTTP и по JSON-контракту
.andExpect(status().isOk())
.andExpect(jsonPath("$.slug").value("spring-basics"));
}
}
Обратите внимание: helper сократил “обвязку”, но тест всё равно говорит человеческим языком: какой endpoint, какой статус, какая смысловая проверка.
Если вы чувствуете, что начинаете писать helper вроде performPublicArticleDetailsRequestAndAssertEverything(...), то это почти всегда знак, что вы склеили два слоя: вы пытаетесь спрятать не шум, а сам сценарий. Для интеграционного теста это особенно опасно: однажды тест упадёт, и вы будете раскручивать “матрёшку” из методов вместо того, чтобы сразу увидеть HTTP-историю на экране.
6. Full-context: меньше моков, больше данных
В @WebMvcTest мы честно признавали: контроллер — это граница HTTP, а всё остальное мы часто подменяем. Поэтому основной болью там были моки: кто что вернул, кто куда был вызван, какие данные мы замокали и не забыли ли мы сценарий “не найдено”.
В full-context тесте типичная боль другая. Теперь зависимости — реальные. И значит главным становится не «настроить мок-скрипт», а создать корректное состояние системы, из которого ваш endpoint должен вернуть результат. На практике это сводится к дисциплине вокруг данных: предсказуемые фикстуры, прозрачный setup и отсутствие “случайного” состояния.
Поэтому в full-context тестах нормально, что рядом с HTTP-сценарием появляется проверка “sanity”: что данные в базе вообще есть, и вы не тестируете пустоту по ошибке.
Например, если вы используете @Sql для загрузки опубликованной статьи, вы можете очень аккуратно проверить, что она действительно появилась, прежде чем вызывать endpoint:
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
class PublicArticleFullContextTest {
@Autowired
ArticleRepository articleRepository; // Реальный репозиторий из контекста (а не мок)
@Test
void sanityCheck_datasetLoaded() {
// Проверка «данные вообще подгрузились», чтобы не ловить ложные падения тестов
Assertions.assertThat(articleRepository.count()).isEqualTo(1);
}
}
И да, это уже другая жизнь по сравнению с @WebMvcTest: в web-slice у вас не было реального ArticleRepository и реальной базы данных.
Ещё один пример дисциплины — использовать @Sql как явный источник правды о состоянии:
@Sql("/sql/public/published-article.sql") // Явно фиксируем состояние БД для сценария тестов
class PublicArticleFullContextTest {
}
Сам SQL мы тут не будем расписывать во всех деталях, потому что схема БД — это отдельная тема, и у вас уже есть миграции Flyway. Но смысл ровно такой: тест перестаёт быть “про моки”, и становится “про состояние системы”.
И вот тут важно не перепутать: это не значит, что теперь надо отказаться от всех helpers. Это значит, что в full-context режиме helpers должны помогать вам не утонуть в шаблонном коде, а не скрывать реальность системы.
Один класс — один доминирующий стиль
Когда у вас в проекте появляются и MockMvc, и MockMvcTester, соблазн очень простой: «в одном тесте напишу вот так, в другом — вот так, а в третьем — как получится». В маленьком проекте это даже переживаемо. Но в реальном test suite это быстро превращается в когнитивную нагрузку: вы открываете класс и каждый раз “переключаете режим чтения”.
В full-context тестах это особенно неприятно, потому что эти тесты и так запускаются медленнее, а чинить их сложнее. Поэтому лучше выбрать одно правило: на один test class — один доминирующий API. Иногда вы оставляете MockMvc как основной и используете MockMvcTester только точечно, или наоборот, но старайтесь, чтобы это было исключением, а не хаосом.
Уместный инженерный компромисс часто выглядит так: вы берёте MockMvc там, где вам нужно много мелких andExpect(...) и вы хотите видеть их “в линию” рядом с запросом. А MockMvcTester берёте там, где вы хотите держаться AssertJ-ритма и где проверок много, но они хорошо группируются по смыслу: сначала статус, потом заголовки, потом тело, потом какие-то точечные JSON-пути.
И ещё важное: не стоит переносить в full-context тесты web-slice привычку «всё через @MockitoBean». Full-context режим ценен именно тем, что зависимости реальные. Подменять стоит только то, что делает тест недетерминированным или нежелательно тяжёлым. Но подменять “потому что так проще” — значит случайно превратить full-context тест обратно в странный гибрид, где вы платите цену полного контекста, но не получаете его ценности.
7. Типичные ошибки при тестировании full-context
Ошибка №1: думать, что @SpringBootTest автоматически даёт MockMvc.
Это очень частая ловушка: студент пишет @SpringBootTest, добавляет @Autowired MockMvc mockMvc;, а затем удивляется, почему бин не найден. В full-context режиме MockMvc появляется не сам по себе, а после явного @AutoConfigureMockMvc. Эта аннотация — не “украшение”, а переключатель режима.
Ошибка №2: переписывать тесты только ради «модности API».
Иногда рука тянется заменить весь raw MockMvc на MockMvcTester, потому что он «красивее». Если от переписывания тест стал короче, но перестал читаться как HTTP-сценарий, вы улучшили стиль и ухудшили доказательность. В тестах такое “улучшение” обычно дорого обходится.
Ошибка №3: прятать смысл теста за “умными” helper-методами.
Helper getJson("/api/public/articles") — нормальный. Helper verifyPublicArticleEndpointWorksForAnyCase(...) — подозрительный. В интеграционных тестах лучше видеть, какой URI вызвали, какие параметры передали и какие поля проверили. Если тест падает, вы хотите сразу увидеть сценарий, а не прыгать по проекту в поисках того, что helper делал внутри.
Ошибка №4: продолжать мыслить full-context тест как web-slice и тащить туда много моков.
Если в вашем full-context тесте половина зависимостей подменена, то вы получили странное существо: контекст дорогой, а доказательная сила — как будто вы снова тестируете изоляцию. В таком режиме вы рискуете платить “цену интеграции”, но ловить дефекты только уровня unit/slice.
Ошибка №5: игнорировать данные и ожидать, что «оно как-то само».
В @WebMvcTest вы сами управляли данными через моки сервисов, поэтому тест легко сделать детерминированным. В full-context тесте нужно явно управлять состоянием через миграции, @Sql, тестовую БД и понятные фикстуры. Если этого не сделать, тесты становятся “по удаче”: иногда возвращается статья, иногда нет, иногда порядок другой, иногда упало из-за неожиданного состояния.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ