Привет, Хабр! Cегодня рассмотрим, как ускорить интеграционные тесты в Spring Boot с помощью специальных slice аннотаций.

Начнём с того, почему вообще тесты могут быть медленными. Используя @SpringBootTest, мы просим Spring Boot поднять весь контекст приложения для каждого тестового класса. У нас доступны все бины, но часто все это избыточно. Например, хочется протестировать контроллер, а Spring загружает ещё и базу данных, и сервисы, и шлёт запросы к Kafka. В результате простой тест метода контроллера может запускаться несколько секунд, пока поднимется веб‑сервер, инициализируется база, подтянутся все классы.

Эту проблему осознали и добавили так называемые test slice‑аннотации. Все простоб грузим не весь контекст, а только срез приложения, например, только веб‑слой или только слой доступа к данным. Spring Boot содержит готовые slice‑аннотации для основных слоёв: @WebMvcTest для веб, @DataJpaTest для JPA‑репозиториев, и ещё пачку для других случаев.

Рассмотрим на примерах двух интересных слайса: @DataJpaTest и @WebMvcTest.

@DataJpaTest – быстрые тесты для JPA и репозитория

Аннотация @DataJpaTest предназначена для тестов, которые используют только JPA‑компоненты — сущности, репозитории, возможно связанные с ними EntityManager и конфигурацию JPA. Когда мы помечаем тест класс @DataJpaTest, Spring Boot:

  • Поднимет только бины, относящиеся к JPA. Включаются автоматические конфигурации для JPA, DataSource, JPA Repositories и связанные компоненты.

  • Отключит сканирование веб‑слоя, безопасности и прочих ненужных штук. Не будут созданы контроллеры, сервисы, веб‑сервер и так далее — ничего лишнего.

  • Автоматически настроит подключение к базе.

  • Сделает тесты транзакционными.

В общем @DataJpaTest готовит лёгкий контекст, где есть всё для работы с базой и JPA, и больше ничего. Благодаря этому запуск такого тестового контекста происходит значительно быстрее, чем полного @SpringBootTest. Нет необходимости инициализировать веб‑сервер, REST‑клиенты, сервис‑слой и так далее

Допустим, есть сущность User и репозиторий UserRepository с методом findByEmail(String email). Хочется протестировать, что поиск по email возвращает нужного пользователя. С полным контекстом пришлось бы поднимать всё приложение, но с @DataJpaTest тест будет узким и быстрым:

@DataJpaTest  
class UserRepositoryTest {  

    @Autowired  
    private TestEntityManager em;  

    @Autowired  
    private UserRepository userRepository;  

    @Test  
    void findByEmailReturnsUser() {  
        // Подготавливаем данные  
        User user = new User(null, "Katya", "katya@example.com");  
        em.persist(user);  // сохраняем пользователя через TestEntityManager  
        em.flush();       // принудительно сбрасываем изменения в БД

        // Выполняем метод репозитория  
        Optional<User> found = userRepository.findByEmail("katya@example.com");  

        // Проверяем результат  
        assertTrue(found.isPresent());  
        assertEquals("Katya", found.get().getName());  
        assertEquals("katya@example.com", found.get().getEmail());  
    }  
}  

Пометили класс теста @DataJpaTest. Spring автоматически поднял базу H2 в памяти, создал нужные таблицы для сущности User. Затем Spring проинжектил TestEntityManager, специальный помощник для JPA‑тестов, и наш UserRepository. В тесте мы сохраняем пользователя через TestEntityManager, он оборачивает обычный EntityManager и упрощает некоторые операции в тестах (например, управляет транзакцией). Можно было бы вместо этого вызвать userRepository.save(user), эффект был бы тот же, просто я показал альтернативу. После этого вызываем findByEmail и убеждаемся, что получили нужного юзера.

Все изменения, которые мы сделали в рамках теста, будут отброшены после завершения метода. @DataJpaTest сам откатит транзакцию. Если запустить следующий тест, база опять чистая, как ни в чём не бывало. Можно гонять сотни тестов подряд, не беспокоясь о всяком мусоре от предыдущих.

Есть только нюансик, по умолчанию @DataJpaTest действительно подключает embedded‑базу. Если хочется прогонять тесты на реальной СУБД, можно отключить подмену DataSource. Для этого используется аннотация @AutoConfigureTestDatabase(replace = NONE), и тогда Spring не будет подставлять H2, попытается использовать настройки из application.properties. В большинстве случаев H2 хватает за глаза, но иногда интеграционные тесты на реальной БД нужны, имейте это в виду.

@WebMvcTest – тестируем контроллеры без лишнего груза

Теперь перейдём к веб‑слою. Аннотация @WebMvcTest предназначена для тестирования Spring MVC контроллеров. Когда мы используем @WebMvcTest, Spring Boot выполняет такой фокус:

  • Поднимает только веб‑слой приложения. В контекст попадут контроллеры, связанные с ними @Component, валидаторы, фильтры и прочие веб‑компоненты.

  • Не поднимает сервисный и репозиторный слои. Бины сервисов, репозитории, DataSource и др. просто не создаются, раз они не нужны для тестирования контроллера. Если контроллер зависит от какого‑то сервиса, вам нужно отдельно замокать эту зависимость.

  • Автоматически конфигурирует удобства для тестирования MVC: например, создает бин MockMvc для отправки HTTP‑запросов в тесте, либо вы можете получить WebTestClient (для WebFlux).

Допустим, есть простой контроллер:

@RestController
@RequestMapping("/api")
class HelloController {
    @Autowired 
    private HelloService service;

    @GetMapping("/hello")
    public String hello(@RequestParam String name) {
        return service.greet(name);
    }
}

Контроллер вызывает HelloService.greet(name) и возвращает строку. Нужно протестировать, что при GET‑запросе к /api/hello?name=Bob контроллер вернёт строку "Hello, Bob!". При этом сам HelloService, допустим, сложный, обращается к БД или ещё куда, а нам не хочется его трогать. Идеальный случай для @WebMvcTest. Пишем тест:

@WebMvcTest(controllers = HelloController.class)  
class HelloControllerTest {  

    @Autowired  
    private MockMvc mockMvc;  

    @MockBean  
    private HelloService helloService;  // замокаем зависимость контроллера

    @Test  
    void helloEndpointReturnsGreeting() throws Exception {  
        // Задаём поведение мок-объекта
        when(helloService.greet("vasya")).thenReturn("Hello, vasya!");

        // Выполняем GET-запрос и проверяем ответ  
        mockMvc.perform(get("/api/hello")
                        .param("name", "vasya"))  
               .andExpect(status().isOk())  
               .andExpect(content().string("Hello, vasya!"));  

        // Дополнительно можно проверить, что сервис вызывался  
        verify(helloService).greet("vasya");  
    }  
}  

Используем @WebMvcTest, указав конкретно HelloController.class в параметре controllers. По дефолту @WebMvcTest подтянет все контроллеры проекта. Если их много, тест опять‑таки будет дольше поднимать контекст. Нам нужна проверка только одного контроллера, лучше так и написать, пусть контекст будет ещё меньше.

Spring поднимет веб‑слой. Зарегистрирует HelloController в контексте, настроит MockMvc и прочие необходимые штучки. Поскольку HelloController зависит от HelloService, а сервисов мы не поднимаем, мы объявляем @MockBean HelloService.

Дальше внутри теста задаём поведение мок‑объекта: при вызове helloService.greet("vasya") вернуть заданную строку. Затем через mockMvc.perform() выполняем GET‑запрос на нужный URL. Результат проверяем через andExpect: статус 200 OK и тело ответа равно «Hello, vasya!». Всё, тест прошёл.

При этом реальный HelloService не вызывался, мы его заменили моком, и самое главное, не было запущено ничего лишнего, ни контекста базы, ни Security, ни прочих контроллеров.

Время запуска такого теста обычно ~2-3 секунды на старт контекста. Кстати, за счёт изоляции можно параллельно запускать тесты разных слоёв, они не будут драться за общий контекст.

Для каждого класса теста с @WebMvcTest контекст уникален. Поэтому даже если в двух тестах мокируются разные сервисы, они не конфликтуют, просто будет два отдельных поднятия контекста. Spring перезапустит контекст между такими тестами, потому что состав бинoв различается. Это нормальное поведение. Главное внутри одного тестового класса старайтесь объявлять все нужные @MockBean сразу, чтобы не перезапускать контекст между методами. Обычно один класс = один контроллер и его зависимости.

Когда какие слайсы применять

В Spring Boot есть и другие slice‑аннотации, перечислю некоторые: @JsonTest (для тестирования сериализации JSON, например проверка работы @JsonComponent или настроек Jackson), @WebFluxTest (аналог WebMvcTest для reactive‑контроллеров), @DataMongoTest (для работы с MongoDB), @JdbcTest (для JDBC‑запросов без JPA) и так далее. Есть даже @GraphQlTest для GraphQL‑контроллеров. Смысл у всех один — ограничить контекст определённым набором бинов.

Возникает вопрос: а что, если мой тест затрагивает несколько слоёв сразу?

Например, хочется протестировать контроллер вместе с сервисом и базой. Тут slice‑аннотации уже не помогут, ведь каждая из них исключает остальные части. В таком случае вы можете:

  • Либо написать отдельный интеграционный тест с @SpringBootTest, если действительно надо проверить взаимодействие всех компонентов сразу.

  • Либо пересмотреть, а нужен ли вам в тесте сразу и веб, и база. Часто лучше написать два теста: один на контроллер с моками, второй на репозиторий.

Slice‑аннотации рассчитаны на изолированное тестирование. Например, слой сервисов обычно тестируют вообще без поднятия Spring — просто JUnit + моки. А вот контроллеры без Spring протестировать сложно, @WebMvcTest тут поможет. Репозиторий без Spring тоже не проверишь, тут выручает @DataJpaTest.

При использовании срезов контекст кэшируется для тестов одного типа. Например, если у вас 5 тест‑классов с @DataJpaTest без дополнительных отличий, Spring поднимет контекст базы один раз и шарит между ними, не будет делать 5 раз. То же с @WebMvcTest, если контроллеры те же самые. Но стоит в разных классах указать разные контроллеры или разных моков, контексты будут разными. В любом случае, это не повод отказываться от slice.

Если вы используете Spring не «по учебнику», а в боевых проектах, полезно системно разложить стек и типовые архитектурные решения. На курсе «Разработчик на Spring Framework» разбираем современные возможности Spring Boot/MVC/Security и reactive-подход, чтобы уверенно собирать микросервисы и доводить фичи до production-grade. Чтобы узнать, подойдет ли вам программа курса, пройдите вступительный тест.

Для знакомства с форматом обучения и экспертами приходите на бесплатные демо-уроки:

  • 25 декабря, 20:00. «Тестирование Spring приложений. Интеграционные тесты с контекстом. Тестирование слоя репозиториев и сервисов». Записаться

  • 15 января, 20:00. «Spring AI: от изображения к данным. Практика распознавания документов». Записаться

  • 22 января, 20:00. «Неожиданное введение в Spring Context». Записаться