Перейти к основному содержимому

Тестирование в Spring Boot

· 10 мин. чтения

1. Обзор

В этом руководстве мы рассмотрим написание тестов с использованием поддержки фреймворка в Spring Boot. Мы рассмотрим модульные тесты, которые могут выполняться изолированно, а также интеграционные тесты, которые загружают контекст Spring перед выполнением тестов.

Если вы новичок в Spring Boot, ознакомьтесь с нашим введением в Spring Boot .

2. Настройка проекта

Приложение, которое мы собираемся использовать в этой статье, представляет собой API, обеспечивающее некоторые базовые операции с ресурсом сотрудника . Это типичная многоуровневая архитектура — вызов API обрабатывается от контроллера к сервису на уровне сохраняемости .

3. Зависимости Maven

Давайте сначала добавим наши тестовые зависимости:

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<version>2.5.0</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>

Spring -boot-starter-test — это основная зависимость, содержащая большинство элементов, необходимых для наших тестов.

База данных H2 — это наша база данных в памяти. Это устраняет необходимость настройки и запуска фактической базы данных для целей тестирования.

3.1. Юнит 4

Начиная с Spring Boot 2.4, старый движок JUnit 5 был удален из spring-boot-starter-test . Если мы все еще хотим писать тесты с использованием JUnit 4, нам нужно добавить следующую зависимость Maven:

<dependency>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-core</artifactId>
</exclusion>
</exclusions>
</dependency>

4. Интеграционное тестирование с помощью @SpringBootTest

Как следует из названия, интеграционные тесты сосредоточены на интеграции различных уровней приложения. Это также означает отсутствие насмешек.

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

Однако в этой статье мы не будем фокусироваться на этом, а вместо этого будем использовать постоянное хранилище H2 в памяти.

Интеграционные тесты должны запускать контейнер для выполнения тестовых случаев. Следовательно, для этого требуется дополнительная настройка — все это легко делается в Spring Boot:

@RunWith(SpringRunner.class)
@SpringBootTest(
SpringBootTest.WebEnvironment.MOCK,
classes = Application.class)
@AutoConfigureMockMvc
@TestPropertySource(
locations = "classpath:application-integrationtest.properties")
public class EmployeeRestControllerIntegrationTest {

@Autowired
private MockMvc mvc;

@Autowired
private EmployeeRepository repository;

// write test cases here
}

Аннотация @SpringBootTest полезна, когда нам нужно загрузить весь контейнер . Аннотация работает, создавая ApplicationContext , который будет использоваться в наших тестах.

Мы можем использовать атрибут webEnvironment @SpringBootTest для настройки нашей среды выполнения; здесь мы используем WebEnvironment.MOCK , чтобы контейнер работал в среде имитации сервлета.

Далее аннотация @TestPropertySource помогает настроить расположение файлов свойств, характерных для наших тестов. Обратите внимание, что файл свойств, загруженный с помощью @TestPropertySource , переопределит существующий файл application.properties .

application-integrationtest.properties содержит сведения о настройке постоянного хранилища:

spring.datasource.url = jdbc:h2:mem:test
spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.H2Dialect

Если мы хотим запустить наши интеграционные тесты для MySQL, мы можем изменить вышеуказанные значения в файле свойств.

Тестовые случаи для интеграционных тестов могут быть похожи на модульные тесты уровня контроллера :

@Test
public void givenEmployees_whenGetEmployees_thenStatus200()
throws Exception {

createTestEmployee("bob");

mvc.perform(get("/api/employees")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(content()
.contentTypeCompatibleWith(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$[0].name", is("bob")));
}

Отличие от модульных тестов уровня контроллера в том, что здесь ничего не имитируется и будут выполняться сквозные сценарии.

5. Тестовая конфигурация с помощью @TestConfiguration

Как мы видели в предыдущем разделе, тест, аннотированный с помощью @SpringBootTest , загрузит весь контекст приложения, что означает, что мы можем @Autowire любой bean-компонент, полученный при сканировании компонентов, в наш тест:

@RunWith(SpringRunner.class)
@SpringBootTest
public class EmployeeServiceImplIntegrationTest {

@Autowired
private EmployeeService employeeService;

// class code ...
}

Однако мы можем захотеть избежать начальной загрузки реального контекста приложения, а использовать специальную тестовую конфигурацию. Мы можем добиться этого с помощью аннотации @TestConfiguration . Есть два способа использования аннотации. Либо в статическом внутреннем классе в том же тестовом классе, где мы хотим @Autowire bean:

@RunWith(SpringRunner.class)
public class EmployeeServiceImplIntegrationTest {

@TestConfiguration
static class EmployeeServiceImplTestContextConfiguration {
@Bean
public EmployeeService employeeService() {
return new EmployeeService() {
// implement methods
};
}
}

@Autowired
private EmployeeService employeeService;
}

В качестве альтернативы мы можем создать отдельный класс конфигурации теста:

@TestConfiguration
public class EmployeeServiceImplTestContextConfiguration {

@Bean
public EmployeeService employeeService() {
return new EmployeeService() {
// implement methods
};
}
}

Классы конфигурации, аннотированные @TestConfiguration , исключаются из сканирования компонентов, поэтому нам нужно явно импортировать их в каждый тест, где мы хотим @Autowire его. Мы можем сделать это с помощью аннотации @Import :

@RunWith(SpringRunner.class)
@Import(EmployeeServiceImplTestContextConfiguration.class)
public class EmployeeServiceImplIntegrationTest {

@Autowired
private EmployeeService employeeService;

// remaining class code
}

6. Насмешки с @MockBean

Код нашего сервисного слоя зависит от нашего репозитория:

@Service
public class EmployeeServiceImpl implements EmployeeService {

@Autowired
private EmployeeRepository employeeRepository;

@Override
public Employee getEmployeeByName(String name) {
return employeeRepository.findByName(name);
}
}

Однако для тестирования уровня службы нам не нужно знать или заботиться о том, как реализован уровень сохраняемости. В идеале мы должны иметь возможность писать и тестировать наш код сервисного уровня без подключения к нашему полному персистентному уровню.

Для этого мы можем использовать поддержку имитации, предоставляемую Spring Boot Test.

Давайте сначала посмотрим на скелет тестового класса:

@RunWith(SpringRunner.class)
public class EmployeeServiceImplIntegrationTest {

@TestConfiguration
static class EmployeeServiceImplTestContextConfiguration {

@Bean
public EmployeeService employeeService() {
return new EmployeeServiceImpl();
}
}

@Autowired
private EmployeeService employeeService;

@MockBean
private EmployeeRepository employeeRepository;

// write test cases here
}

Чтобы проверить класс Service , нам нужно создать экземпляр класса Service и сделать его доступным как @Bean , чтобы мы могли @Autowire его в нашем тестовом классе. Мы можем добиться этой конфигурации, используя аннотацию @TestConfiguration .

Еще одна интересная вещь здесь — использование @MockBean . Он создает Mock для EmployeeRepository , который можно использовать для обхода вызова фактического EmployeeRepository :

@Before
public void setUp() {
Employee alex = new Employee("alex");

Mockito.when(employeeRepository.findByName(alex.getName()))
.thenReturn(alex);
}

Поскольку настройка завершена, тестовый пример будет проще:

@Test
public void whenValidName_thenEmployeeShouldBeFound() {
String name = "alex";
Employee found = employeeService.getEmployeeByName(name);

assertThat(found.getName())
.isEqualTo(name);
}

7. Интеграционное тестирование с @DataJpaTest

Мы собираемся работать с сущностью с именем Employee, которая имеет идентификатор и имя в качестве свойств:

@Entity
@Table(name = "person")
public class Employee {

@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;

@Size(min = 3, max = 20)
private String name;

// standard getters and setters, constructors
}

А вот наш репозиторий с использованием Spring Data JPA:

@Repository
public interface EmployeeRepository extends JpaRepository<Employee, Long> {

public Employee findByName(String name);

}

Вот и все, что касается кода слоя сохранения. Теперь давайте приступим к написанию нашего тестового класса.

Во-первых, давайте создадим скелет нашего тестового класса:

@RunWith(SpringRunner.class)
@DataJpaTest
public class EmployeeRepositoryIntegrationTest {

@Autowired
private TestEntityManager entityManager;

@Autowired
private EmployeeRepository employeeRepository;

// write test cases here

}

@RunWith(SpringRunner.class) обеспечивает мост между функциями тестирования Spring Boot и JUnit. Всякий раз, когда мы используем какие-либо функции тестирования Spring Boot в наших тестах JUnit, эта аннотация потребуется.

@DataJpaTest предоставляет некоторые стандартные настройки, необходимые для тестирования уровня сохраняемости:

  • настройка H2, базы данных в памяти
  • установка Hibernate, Spring Data и DataSource
  • выполнение @EntityScan
  • включение ведения журнала SQL

Для выполнения операций с БД нам нужны некоторые записи, которые уже есть в нашей базе данных. Чтобы настроить эти данные, мы можем использовать TestEntityManager.

Spring Boot TestEntityManager — это альтернатива стандартному JPA EntityManager , который предоставляет методы, обычно используемые при написании тестов.

EmployeeRepository — это компонент, который мы собираемся протестировать.

Теперь давайте напишем наш первый тестовый пример:

@Test
public void whenFindByName_thenReturnEmployee() {
// given
Employee alex = new Employee("alex");
entityManager.persist(alex);
entityManager.flush();

// when
Employee found = employeeRepository.findByName(alex.getName());

// then
assertThat(found.getName())
.isEqualTo(alex.getName());
}

В приведенном выше тесте мы используем TestEntityManager для вставки сотрудника в БД и чтения его с помощью API поиска по имени.

Часть assertThat(…) происходит из библиотеки Assertj , которая поставляется в комплекте с Spring Boot.

8. Модульное тестирование с помощью @WebMvcTest

Наш контроллер зависит от сервисного уровня; давайте для простоты включим только один метод:

@RestController
@RequestMapping("/api")
public class EmployeeRestController {

@Autowired
private EmployeeService employeeService;

@GetMapping("/employees")
public List<Employee> getAllEmployees() {
return employeeService.getAllEmployees();
}
}

Поскольку мы сосредоточены только на коде контроллера , вполне естественно имитировать код сервисного уровня для наших модульных тестов:

@RunWith(SpringRunner.class)
@WebMvcTest(EmployeeRestController.class)
public class EmployeeRestControllerIntegrationTest {

@Autowired
private MockMvc mvc;

@MockBean
private EmployeeService service;

// write test cases here
}

Чтобы протестировать контроллеры , мы можем использовать @WebMvcTest . Он автоматически настроит инфраструктуру Spring MVC для наших модульных тестов.

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

@WebMvcTest также автоматически настраивает MockMvc , который предлагает мощный способ простого тестирования контроллеров MVC без запуска полноценного HTTP-сервера.

Сказав это, давайте напишем наш тестовый пример:

@Test
public void givenEmployees_whenGetEmployees_thenReturnJsonArray()
throws Exception {

Employee alex = new Employee("alex");

List<Employee> allEmployees = Arrays.asList(alex);

given(service.getAllEmployees()).willReturn(allEmployees);

mvc.perform(get("/api/employees")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$", hasSize(1)))
.andExpect(jsonPath("$[0].name", is(alex.getName())));
}

Вызов метода get(…) можно заменить другими методами, соответствующими глаголам HTTP, таким как put() , post() и т. д. Обратите внимание, что мы также устанавливаем тип контента в запросе.

MockMvc гибкий, и с его помощью мы можем создать любой запрос.

9. Автоматически настраиваемые тесты

Одной из замечательных особенностей автоматически настраиваемых аннотаций Spring Boot является то, что они помогают загружать части полного приложения и специфичные для тестирования слои кодовой базы.

В дополнение к вышеупомянутым аннотациям, вот список нескольких широко используемых аннотаций:

  • @WebF luxTest : мы можем использовать аннотацию @WebFluxTest для тестирования контроллеров Spring WebFlux. Он часто используется вместе с @MockBean для предоставления фиктивных реализаций необходимых зависимостей.
  • @JdbcTest : мы можем использовать аннотацию @JdbcTest для тестирования JPA-приложений, но это для тестов, которым требуется только DataSource. Аннотация настраивает встроенную базу данных в памяти и шаблон JdbcTemplate. ``
  • @JooqTest : для тестирования тестов, связанных с jOOQ, мы можем использовать аннотацию @JooqTest , которая настраивает DSLContext.
  • @DataMongoTest : для тестирования приложений MongoDB полезная аннотация @DataMongoTest . По умолчанию он настраивает встроенную в память MongoDB, если драйвер доступен через зависимости, настраивает MongoTemplate, сканирует классы @Document и настраивает репозитории Spring Data MongoDB.
  • @DataRedisTest упрощает тестирование приложений Redis. Он сканирует классы @RedisHash и настраивает репозитории Spring Data Redis по умолчанию.
  • @DataLdapTest настраивает встроенный в память LDAP (если он доступен), настраивает LdapTemplate , сканирует классы @Entry и настраивает репозитории Spring Data LDAP по умолчанию.
  • @RestClientTest : обычно мы используем аннотацию @RestClientTest для тестирования клиентов REST. Он автоматически настраивает различные зависимости, такие как поддержка Jackson, GSON и Jsonb; настраивает RestTemplateBuilder; и добавляет поддержку MockRestServiceServer по умолчанию.
  • @JsonTest : инициализирует контекст приложения Spring только теми bean-компонентами, которые необходимы для тестирования сериализации JSON.

Подробнее об этих аннотациях и способах дальнейшей оптимизации интеграционных тестов вы можете прочитать в нашей статье Optimizing Spring Integration Tests .

10. Заключение

В этой статье мы подробно рассмотрели поддержку тестирования в Spring Boot и показали, как эффективно писать модульные тесты.

Полный исходный код этой статьи можно найти на GitHub . Исходный код содержит еще много примеров и различных тестовых случаев.

А если вы хотите продолжить изучение тестирования, у нас есть отдельные статьи, посвященные интеграционным тестам , оптимизации интеграционных тестов Spring и модульным тестам в JUnit 5 .