[Перевод] Сравнение SpringRunner со SpringExtension и @SpringBootTest

Команда 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 предоставляет инструменты, которые пригодятся нам, чтобы достичь такого баланса. 

Приятного тестирования.

25848492d4cd2dcd1df595e475569a9a.png

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

© Habrahabr.ru