Команда Spring АйО перевела статью о том, как и когда использовать SpringRunner, SpringExtension и @SpringBootTest, когда их целесообразно комбинировать и как правильное понимание этих компонентов может помочь сделать тесты проще, быстрее и более узконаправленными.
Эффективное тестирование Spring Boot приложений требует понимания тестовой инфраструктуры, которую предлагает Spring.
Три часто встречающихся компонента — SpringRunner, SpringExtension и @SpringBootTest — часто запутывают разработчиков. Многие просто вставляют эти аннотации куда попало, не понимая их предназначения и как именно они дополняют друг друга.
В этой статье мы разъясним различия между этими тестовыми компонентами, разберем, когда используется каждый из них и продемонстрируем, как они работают вместе. По итогу у нас появится четкое понимание тестовой экосистемы Spring, и мы сможем писать более эффективные тесты для наших приложений на Spring Boot.
Эволюция JUnit: SpringRunner и SpringExtension
Первым запутанным моментом часто оказывается то, в каких отношениях находятся между собой SpringRunner и SpringExtension.
Давайте объясним это, обратившись к предыстории и приведя несколько примеров кода.
SpringRunner — подход, как в JUnit 4
SpringRunner предназначен для запуска тестов в JUnit 4, который строит мост между Spring и фреймворком для запуска тестов из JUnit 4.
Это алиас для SpringJUnit4ClassRunner, который пред��ставляет основную функциональность, необходимую для запуска тестов, которые используют тестовые возможности из Spring.
Приведем пример тестового класса JUnit 4, использующего SpringRunner:
import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit4.SpringRunner; @RunWith(SpringRunner.class) @SpringBootTest public class UserServiceJUnit4Test { @Autowired private UserService userService; @Test public void testUserCreation() { User user = new User("test@example.com", "password"); User savedUser = userService.createUser(user); assertNotNull(savedUser.getId()); assertEquals("test@example.com", savedUser.getEmail()); } }
Аннотация @RunWith(SpringRunner.class) предписывает JUnit 4 использовать функциональность поддержки тестов от Spring.
Она отвечает за создание контекста приложения, разрешение инжекции зависимостей и управление событиями жизненного цикла теста.
SpringExtension — подход, как в JUnit 5
В JUnit 5 (Jupiter) модель расширения (extension) заменила собой концепцию запускающего тест класса (runner), свойственную JUnit 4. SpringExtension является реализацией этой новой модели расширения, предлагаемой Spring и предоставляющей нам ту же функциональность, что и у SpringRunner, но для JUnit 5.
Далее приводится тот же тест, что и выше, но переписанный для JUnit 5 с использованием SpringExtension:
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit.jupiter.SpringExtension; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; @ExtendWith(SpringExtension.class) @SpringBootTest class UserServiceJUnit5Test { @Autowired private UserService userService; @Test void testUserCreation() { User user = new User("test@example.com", "password"); User savedUser = userService.createUser(user); assertNotNull(savedUser.getId()); assertEquals("test@example.com", savedUser.getEmail()); } }
Ключевая разница состоит в использовании @ExtendWith(SpringExtension.class) вместо @RunWith(SpringRunner.class). Обе аннотации служат выполнению одной и той же цели: интегрируют тестовую инфраструктуру Spring с соответствующей версией JUnit.
Понимание @SpringBootTest
В то время как SpringRunner и SpringExtension предоставляют интеграцию между Spring и JUnit, @SpringBootTest определяет, что и как тестировать. Эта аннотация конфигурирует контекст приложения на Spring для наших тестов, загружая полную конфигурацию приложения.
Аннотация @SpringBootTest предлагает различные свойства для кастомизации нашего тестового окружения:
import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.ActiveProfiles; import static org.junit.jupiter.api.Assertions.assertEquals; @SpringBootTest( webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, properties = {"spring.datasource.url=jdbc:h2:mem:testdb"} ) @ActiveProfiles("test") class ConfiguredApplicationTest { @Autowired private UserRepository userRepository; @Test void testUserRepository() { userRepository.save(new User("admin@example.com", "admin")); User foundUser = userRepository.findByEmail("admin@example.com").orElse(null); assertEquals("admin@example.com", foundUser.getEmail()); } }
В этом примере:
Мы задаем
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, чтобы запустить встроенный сервер со случайным портомМы перезаписываем свойство с помощью
properties = {"spring.datasource.url=jdbc:h2:mem:testdb"}Мы активируем профиль “test” с помощью
@ActiveProfiles("test")
Эти конфигурации позволяют нам идеально контролировать наше тестовое окружение, не меняя код приложения.
Как комбинировать компоненты: Best Practices
Теперь когда мы понимаем каждый компонент в отдельности, давайте разберемся, как они работают вместе и какие best practices применимы к различным тестовым сценариям.
JUnit 5: упрощенная конфигурация
В Spring Boot 2.2.0 и позднее в JUnit 5 аннотация @SpringBootTest включает @ExtendWith(SpringExtension.class) по умолчанию, позволяя нам упростить код нашего теста:
import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; @SpringBootTest class SimplifiedUserServiceTest { @Autowired private UserService userService; @Test void testUserCreation() { User user = new User("test@example.com", "password"); User savedUser = userService.createUser(user); assertNotNull(savedUser.getId()); assertEquals("test@example.com", savedUser.getEmail()); } }
Этот упрощенный подход рекомендуется для тестов на JUnit 5. Однако, если мы используем JUnit 4, нам все еще необходимо включать @RunWith(SpringRunner.class) в явном виде.
Выбор правильного охвата для теста
В то время как @SpringBootTest загружает полный контекст приложения, иногда мы хотим написать более узконаправленный тест. Spring предлагает дополнительные тестовые аннотации для различных охватов:
Тестирование веб слоя с помощью MockMvc
@WebMvcTest(UserController.class) class UserControllerTest { @Autowired private MockMvc mockMvc; @MockBean private UserService userService; @Test void testCreateUser() throws Exception { User mockUser = new User("test@example.com", "password"); mockUser.setId(1L); when(userService.createUser(any(User.class))).thenReturn(mockUser); mockMvc.perform(post("/api/users") .contentType("application/json") .content("{\"email\":\"test@example.com\",\"password\":\"password\"}")) .andExpect(status().isCreated()) .andExpect(jsonPath("$.id").value(1)) .andExpect(jsonPath("$.email").value("test@example.com")); } }
@WebMvcTest фокусируется на тестировании только веб слоя и неявно включает SpringExtension. Она быстрее @SpringBootTest, потому что загружает только те компоненты, которые относятся к веб.
Тестирование слоя данных
@DataJpaTest class UserRepositoryTest { @Autowired private UserRepository userRepository; @Test void testFindByEmail() { User user = new User("test@example.com", "password"); userRepository.save(user); assertTrue(userRepository.findByEmail("test@example.com").isPresent()); assertEquals("test@example.com", userRepository.findByEmail("test@example.com").get().getEmail()); } }
@DataJpaTest фокусируется на тестировании JPA компонентов, конфигурирует in-memory базу данных и включает только относящиеся к JPA бины. Как и @WebMvcTest, она неявно включает SpringExtension.
Заключение
Понимание различий между SpringRunner, SpringExtension и @SpringBootTest помогает нам писать более эффективные тесты для приложений на Spring Boot:
SpringRunnerпредназначен для JUnit 4 и интегрирует возможности Spring в области тестирования с моделью запускающего класса JUnitSpringExtensionпредназначен для JUnit 5 и предоставляет эквивалентную функциональность, используя модель расширения от Jupiter@SpringBootTestконфигурирует контекст приложения для тестов и может быть кастомизирована с помощью различных свойств
Используя Spring Boot 2.2.0+ и JUnit 5, мы можем упростить наши тесты, просто используя @SpringBootTest и не добавляя @ExtendWith(SpringExtension.class) в явном виде. Для JUnit 4 нам все еще нужна аннотация @RunWith(SpringRunner.class).
Выбирая правильную область охвата теста — будет ли это тест всего приложения, выполняемый с помощью @SpringBootTest, тест веб-слоя, использующий @WebMvcTest или тест слоя данных с @DataJpaTest — мы можем делать наши тесты более сфокусированными, быстрыми и простыми в поддержке.
Помните, что наша цель состоит в том, чтобы эффективно тестировать наши приложения, при этом сохраняя простоту и читаемость тестов. Фреймворк для тестирования от Spring предоставляет инструменты, которые пригодятся нам, чтобы достичь такого баланса.
Приятного тестирования.

Присоединяйтесь к русскоязычному сообществу разработчиков на Spring Boot в телеграм — Spring АйО, чтобы быть в курсе последних новостей из мира разработки на Spring Boot и всего, что с ним связано.
