Команда 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 в области тестирования с моделью запускающего класса JUnit

  • SpringExtension предназначен для 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 и всего, что с ним связано.