JavaRush /Курсы /Spring Test /Зависимости контроллера в slice‑тесте

Зависимости контроллера в slice‑тесте

Spring Test
9 уровень , 2 лекция
Открыта

1. Дефицит зависимостей в @WebMvcTest

Когда контроллер уже выбран и slice не расползся на весь API, появляется следующий очень приземлённый вопрос: чем закрыть его зависимости.

Если вы впервые запускаете @WebMvcTest, он часто встречает вас не зелёным тестом, а очень воспитанным падением контекста. И это на самом деле хорошая новость: Spring прямо говорит «я попытался создать контроллер как настоящий bean, но ты не дал мне то, что ему нужно». В обычном приложении зависимости контроллера приходят из ApplicationContext, а в slice‑тесте этот контекст намеренно урезан.

Представьте, что @WebMvcTest — это проверка работы «пункта выдачи заказов» (контроллера) без складов и производства (репозиториев и БД). Пункт выдачи должен уметь принять запрос, спросить «у системы» нужные данные и корректно отдать ответ клиенту. Но в тесте «система» (service‑layer) не поднята — и это нормально: мы сейчас не доказываем бизнес‑правила, мы проверяем HTTP‑границу.

Наглядно граница выглядит примерно так:

flowchart TD
    R[HTTP-запрос] --> MVC[Spring MVC инфраструктура]
    MVC --> C[PublicArticleController]
    C --> S["PublicArticleService в тесте подменяем"]
    S -.-> DB["БД / репозитории в slice не поднимаются"]

Контроллер почти всегда написан через constructor injection (и в нашем проекте это правило фиксировано). Поэтому если в контексте нет бина PublicArticleService, Spring не сможет создать PublicArticleController, и тест не стартует. Для slice‑теста это ожидаемо: мы должны явно закрыть зависимости контроллера — обычно через mock, иногда через spy, иногда через импорт маленького вспомогательного компонента.

Мини‑пример «как выглядит зависимость у контроллера»:

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/public/articles")
class PublicArticleController {

    // Зависимость контроллера: в реальном приложении придёт из ApplicationContext,
    // а в @WebMvcTest её нужно подложить явно (обычно через @MockitoBean).
    private final PublicArticleService publicArticleService;

    // Constructor injection: если бина нет — Spring не сможет создать контроллер,
    // и slice-тест упадёт на старте контекста.
    PublicArticleController(PublicArticleService publicArticleService) {
        this.publicArticleService = publicArticleService;
    }
}

Здесь контроллер честно говорит: «Мне нужен PublicArticleService». В @WebMvcTest этого бина, скорее всего, не будет. И вот тут на сцену выходит @MockitoBean.

2. @MockitoBean: Mockito‑mock, который видит Spring

На уровне чистого Mockito мы уже умеем писать @Mock и получать красивый «резиновый объект», который делает только то, что мы ему заранее сказали. Но в @WebMvcTest недостаточно иметь mock как обычное поле тестового класса — контроллер же создаёт Spring, и зависимости ему тоже подсовывает Spring. Значит, нам нужен mock, который зарегистрирован внутри ApplicationContext как bean.

@MockitoBean делает ровно это: создаёт Mockito‑mock и регистрирует его как Spring bean (и/или заменяет существующий bean такого типа в контексте теста). В результате конструктор контроллера получает не настоящий сервис, а наш управляемый mock.

В этой линейке курса и @MockitoBean, и @MockitoSpyBean мы берём из spring-testorg.springframework.test.context.bean.override.mockito.... Это и есть наш канонический path для современного Boot 4 / Spring 7 стека.

Частая «ошибка по привычке» выглядит так (и она не решает проблему зависимости):

import org.mockito.Mock;

@WebMvcTest(PublicArticleController.class)
class PublicArticleControllerWebMvcTest {

    // Mockito mock, но НЕ Spring bean: Spring не знает, что это за объект,
    // и не сможет внедрить его в контроллер через DI.
    @Mock
    PublicArticleService publicArticleService;
}

Да, publicArticleService будет mock’ом. Но Spring при создании контроллера вообще о нём не узнает, потому что этот mock не зарегистрирован как bean. В итоге контекст всё равно не стартует.

А вот так — правильно для MVC slice:

import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.test.context.bean.override.mockito.MockitoBean;

@WebMvcTest(PublicArticleController.class)
class PublicArticleControllerWebMvcTest {

    // Mockito mock, зарегистрированный в Spring-контексте теста:
    // контроллер получит именно его через constructor injection.
    @MockitoBean
    PublicArticleService publicArticleService;
}

Здесь PublicArticleService становится полноценным участником контекста теста. Для Spring это «обычный bean», просто его поведение мы контролируем Mockito‑стаббингом.

Есть ещё один важный практический момент. В slice‑тестах мы стараемся, чтобы каждый тестовый метод был самодостаточным и не зависел от «настроек где‑то сверху». Поэтому stubbing обычно пишут прямо в тесте (или в очень аккуратных helper‑методах), а не один раз «на всё» в @BeforeEach. Это делает тесты понятнее и снижает шанс, что один сценарий случайно влияет на другой.

3. Stubbing для контроллера: задаём смысл ответа

Когда у нас есть @MockitoBean, следующий шаг — научиться задавать контроллеру предсказуемое внешнее поведение его зависимости. И тут важно держать мысль: в controller‑тесте сервис для нас — это «чёрный ящик», который по входу возвращает результат. Мы не проверяем, как он его получил, и не пытаемся отрепетировать все внутренние вызовы. Мы просто говорим: «когда контроллер попросит X — верни Y».

Для начала удобно использовать BDD‑стиль Mockito: given(...).willReturn(...). Он читается почти как человеческий язык: «дано, что сервис вернёт вот это».

Пусть публичная карточка статьи отдаётся DTO (в проекте это нормальная практика: наружу — только DTO):

// DTO для наружного контракта: то, что реально увидит клиент.
public record ArticleDetailsResponse(
    String slug,
    String title,
    String summary
) {}

Теперь в тесте мы хотим: по slug "spring-slice" контроллер возвращает 200 OK и JSON с title = "Spring Slice".

Скелет теста (пока без деталей HTTP‑механики, только чтобы увидеть связку «stub → вызов → ответ»):

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.web.servlet.MockMvc;

@WebMvcTest(PublicArticleController.class)
class PublicArticleControllerWebMvcTest {

    // MockMvc — наш «клиент» для вызова endpoint'ов контроллера.
    @Autowired MockMvc mockMvc;

    // Заменяем сервис на mock, чтобы тест проверял web-границу,
    // а не поднимал бизнес-слой.
    @MockitoBean PublicArticleService publicArticleService;

    @Test
    void returnsArticleDetails() throws Exception {
        // stubbing будет здесь
    }
}

Сам stubbing — короткий и «смысловой»:

import static org.mockito.BDDMockito.given;

// Мы задаём предсказуемый ответ сервиса для конкретного входа (slug),
// чтобы контроллер мог сформировать стабильный HTTP-ответ.
given(publicArticleService.getBySlug("spring-slice"))
    .willReturn(new ArticleDetailsResponse(
        "spring-slice",
        "Spring Slice",
        "Short summary"
    ));

Обратите внимание на тонкость: мы возвращаем именно DTO, которое контроллер отдает наружу. Это идеально ложится в идею controller‑теста: проверить web‑контракт и «проводку», а не бизнес‑логику.

Если хочется увидеть, что stub действительно влияет на результат контроллера, минимальный вызов через MockMvc может выглядеть так:

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

// Вызываем endpoint так, как это сделал бы клиент, и проверяем базовый контракт:
// контроллер отвечает 200 OK.
mockMvc.perform(get("/api/public/articles/spring-slice"))
    .andExpect(status().isOk());

Заметьте, мы здесь не расписываем богатые проверки тела ответа. Это сознательно: в рамках разговора про зависимости нам достаточно понять, что контроллер «живёт» и получает данные от замоканного сервиса. Чем больше вы нагрузите этот пример JSON‑проверками, тем быстрее вы уедете в тему HTTP‑механики вместо темы «как правильно подменять зависимости».

4. verify(...) в controller‑тестах

После того как вы научились стаббить сервис, руки обычно тянутся к verify(...): «а давайте ещё проверим, что сервис точно был вызван». И тут очень легко перейти границу между полезной проверкой и «проверкой сценария вызовов ради самого сценария».

В controller‑тесте verify обычно уместен, когда вы хотите защитить именно web‑смысл. Например, вы хотите доказать, что path variable {slug} действительно передаётся в сервис, а не игнорируется или не подменяется чем-то странным. Это не проверка бизнес‑логики, это проверка правильной связки web → service boundary.

Мини‑пример:

import static org.mockito.Mockito.verify;

// Проверяем важный web-смысл: что slug из URL реально передали в сервис.
verify(publicArticleService).getBySlug("spring-slice");

А вот где verify становится сомнительным: когда вы начинаете проверять количество вызовов «на всякий случай», порядок вызовов, и особенно когда добавляете verifyNoMoreInteractions(...) на каждый тест. В web‑слое легко появится маленький технический вызов (например, логирование, метрика, подготовка контекста), и ваш тест начнёт падать не потому, что сломался контракт API, а потому, что «контроллер стал делать на один вызов больше». Это и есть классическая хрупкость.

Хорошее внутреннее правило звучит так: если убрать verify, и тест всё равно доказывает внешний контракт endpoint’а — чаще всего verify не обязателен. Если без verify тест перестаёт защищать важный смысл (например, корректное использование входных параметров) — тогда verify оправдан.

5. @MockitoSpyBean: реальный Spring‑bean, но под микроскопом

Spy — это инструмент, который звучит как «мок, но по‑взрослому». На практике он ближе к фразе «оставь всё как есть, но дай мне в одном месте вмешаться». У spy по умолчанию вызываются реальные методы объекта, а Mockito позволяет либо подсмотреть вызовы, либо точечно переопределить поведение пары методов.

@MockitoSpyBean делает spy не для обычного объекта, а для Spring‑бина внутри контекста теста. То есть Spring создаёт настоящий bean, а мы получаем поверх него «обёртку‑шпиона».

В web slice это может быть полезно, когда рядом с контроллером есть небольшой чистый компонент, который вы считаете частью web‑слоя, и вы не хотите мокать его полностью. Типичный пример — простой mapper, formatter или небольшой helper без I/O.

Важно помнить: чтобы @MockitoSpyBean сработал, бин должен реально существовать в контексте. Если компонент не попадает в slice автоматически, его нужно зарегистрировать в контексте (как именно — вопрос техники, но смысл простой: без реального бина шпионить не за чем).

Мини‑скелет, как это выглядит в тесте:

import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.bean.override.mockito.MockitoSpyBean;

@WebMvcTest(PublicArticleController.class)
@Import(PublicArticleMapper.class) // бин должен быть в контексте
class PublicArticleControllerWebMvcTest {

    // Spy оборачивает РЕАЛЬНЫЙ бин: по умолчанию будут вызваны настоящие методы,
    // но мы можем точечно подменить поведение или проверить вызовы.
    @MockitoSpyBean
    PublicArticleMapper publicArticleMapper;
}

Теперь два очень практичных нюанса про spy.

Первый: spy вызывает реальные методы, значит, если реальный метод делает что-то тяжёлое или нестабильное (I/O, время, сеть, файловую систему), вы получите «весёлые» тесты: медленные, flaky и не очень объяснимые. Для таких зависимостей spy — плохая идея. Там лучше либо mock, либо реальный тест на другом уровне.

Второй: когда вы хотите застаббить метод spy, часто безопаснее использовать стиль doReturn(...).when(...), а не when(spy.method(...)).thenReturn(...). Причина простая и противная: when(spy.method()) в момент настройки может реально вызвать метод, а он может упасть или сделать лишнюю работу.

Небольшой пример «аккуратного» stubbing для spy:

import static org.mockito.Mockito.doReturn;

// Важно: doReturn(...) не вызывает реальный метод при настройке стаба.
doReturn("Spring Slice")
    .when(publicArticleMapper)
    .normalizeTitle("SPRING SLICE");

Даже если normalizeTitle в реальности делает что-то сложнее (или неожиданно падает на null), doReturn гарантирует, что при настройке stub реальный метод не дернётся.

И ещё раз: @MockitoSpyBean — это инструмент на редкие случаи. В большинстве controller‑тестов вам хватает @MockitoBean для сервисов и реальных MVC‑компонентов.

6. Выбор: real bean, mock или spy

Когда у вас в руках есть и mock, и spy, появляется соблазн «контролировать вообще всё». Но цель slice‑теста — не создать кукольный театр, где каждое движение прописано. Цель — быстро и локально проверить поведение web‑границы. Поэтому полезно держать простой ориентир выбора и не усложнять раньше времени.

Ниже — компактная таблица‑шпаргалка, которая обычно спасает от лишней магии:

Что вы делаете с зависимостью контроллера Как ведёт себя по умолчанию Когда это уместно в MVC slice Главный риск
Оставляете реальный bean Работает как в приложении Если это часть web‑слоя и она маленькая/детерминированная (например, простой mapper) Тест станет сложнее из‑за «лишней» реальной логики
Ставите @MockitoBean Делает только то, что вы застаббили Для границы контроллера: сервисы/фасады, которые не входят в web‑slice и не должны быть проверены здесь Можно нечаянно замокать слишком много и потерять смысл теста
Ставите @MockitoSpyBean Вызывает реальный код, но вы можете вмешаться Редко: когда хотите сохранить реальное поведение, но нужно точечно подменить 1–2 детали или посмотреть вызовы Легко получить side effects, хрупкость и странные падения

Если сомневаетесь — почти всегда начинайте с @MockitoBean для сервисов. Spy стоит доставать только когда вы точно понимаете, зачем вам нужно «частично реальное» поведение.

Здесь полезно отделить два класса зависимостей. Сервисы и фасады почти всегда остаются снаружи controller slice и идут через @MockitoBean. А вот маленькие соседи самой web-границы — mapper, @ControllerAdvice, properties-based helper — это уже другой разговор: их иногда выгоднее оставить реальными и добавить точечно, а не превращать всё в кукольный театр.

7. @WebMvcTest без кукольного театра

Самая коварная проблема controller‑тестов — не в том, что они сложные. А в том, что их очень легко сделать зелёными и бесполезными. Если замокать вообще всё (включая мапперы, конвертеры, хелперы), вы в итоге тестируете не контроллер, а собственные заглушки. Это особенно опасно для начинающих: тесты зелёные, уверенность высокая, а реальный endpoint ломается от первого же изменения wiring’а.

Держите в голове простую границу. В @WebMvcTest мы проверяем, что MVC‑слой правильно обрабатывает запрос и формирует ответ. Значит, мы оставляем реальными контроллер, Spring MVC инфраструктуру и всё, что реально относится к web‑поведению. А вот сервисный слой — это внешняя зависимость по отношению к контроллеру, и его почти всегда нужно подменить через @MockitoBean.

Есть и архитектурный сигнал, который хорошо ловится именно на этом этапе. Если ваш контроллер требует пять разных сервисов, два маппера, один «менеджер транзакций на всякий случай» и ещё три загадочных фасада, то тест с @MockitoBean внезапно станет огромным и неудобным. И проблема тут обычно не в тесте, а в том, что контроллер перегружен ответственностями. Slice‑тесты полезны ещё и тем, что показывают такие места без долгих рассуждений: «Слишком много моков = слишком много обязанностей в одном месте».

8. Типичные ошибки при использовании @MockitoBean и @MockitoSpyBean

Когда переходишь от unit‑тестов к @WebMvcTest, кажется, что всё то же самое, только с аннотацией сверху. На практике здесь есть несколько граблей, на которые наступают почти все, и это нормально. Важно просто узнавать их по звуку и не спорить с реальностью (она всё равно победит).

Ошибка №1: использовать @Mock, ожидая, что контроллер его получит через DI.
@Mock создаёт mock для Mockito, но Spring о нём не знает. Контекст будет падать с отсутствующим bean’ом зависимости. В slice‑тестах, где контроллер создаёт Spring, зависимости контроллера тоже нужно давать Spring’у — через @MockitoBean.

Ошибка №2: стаббить «всё и сразу» в @BeforeEach, а потом теряться, что именно важно в тесте.
Так тест превращается в квест «найди, где настроено нужное поведение». Гораздо читабельнее, когда stubbing лежит рядом с конкретным сценарием. Тогда тест читается как история: какие данные мы ожидаем от сервиса и что контроллер с ними делает.

Ошибка №3: превращать каждый controller‑тест в проверку количества вызовов через verify(...).
Проверки взаимодействий полезны, но только когда они защищают смысл. Если вы проверяете каждый вызов «на всякий случай», тесты начинают ломаться от мелких технических правок, хотя внешний контракт endpoint’а не изменился. В итоге вы чините тесты, а не защищаете API.

Ошибка №4: использовать @MockitoSpyBean на бинах с побочными эффектами.
Spy вызывает реальный код. Если реальный код ходит во внешние ресурсы, читает файлы, зависит от времени или просто «тяжёлый», вы получите медленные и нестабильные тесты. Spy уместен только для очень предсказуемых компонентов, которые не делают I/O и не тянут за собой половину приложения.

Ошибка №5: стаббить spy через when(spy.method())...
Это классика Mockito: вы хотели «подменить поведение», но в момент stubbing вызвали настоящий метод, и он упал. Для spy безопаснее использовать doReturn(...).when(...) (особенно если метод может падать на неготовых данных).

1
Задача
Spring Test, 9 уровень, 2 лекция
Недоступна
Подмена зависимости контроллера через `@MockitoBean`
Подмена зависимости контроллера через `@MockitoBean`
1
Задача
Spring Test, 9 уровень, 2 лекция
Недоступна
Аккуратный spy на реальном bean внутри slice
Аккуратный spy на реальном bean внутри slice
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ