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 DataLDAP
по умолчанию.
@RestClientTest
: обычно мы используем аннотацию@RestClientTest
для тестирования клиентов REST. Он автоматически настраивает различные зависимости, такие как поддержка Jackson, GSON и Jsonb; настраиваетRestTemplateBuilder;
и добавляет поддержкуMockRestServiceServer
по умолчанию.@JsonTest
: инициализирует контекст приложения Spring только теми bean-компонентами, которые необходимы для тестирования сериализации JSON.
Подробнее об этих аннотациях и способах дальнейшей оптимизации интеграционных тестов вы можете прочитать в нашей статье Optimizing Spring Integration Tests .
10. Заключение
В этой статье мы подробно рассмотрели поддержку тестирования в Spring Boot и показали, как эффективно писать модульные тесты.
Полный исходный код этой статьи можно найти на GitHub . Исходный код содержит еще много примеров и различных тестовых случаев.
А если вы хотите продолжить изучение тестирования, у нас есть отдельные статьи, посвященные интеграционным тестам , оптимизации интеграционных тестов Spring и модульным тестам в JUnit 5 .