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

Интеграционное тестирование весной

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

Задача: Сумма двух чисел

Напишите функцию twoSum. Которая получает массив целых чисел nums и целую сумму target, а возвращает индексы двух чисел, сумма которых равна target. Любой набор входных данных имеет ровно одно решение, и вы не можете использовать один и тот же элемент дважды. Ответ можно возвращать в любом порядке...

ANDROMEDA

1. Обзор

Интеграционное тестирование играет важную роль в цикле разработки приложений, проверяя сквозное поведение системы.

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

2. Подготовка

Нам понадобится несколько зависимостей Maven для запуска интеграционных тестов, которые мы будем использовать в этой статье. Прежде всего, нам понадобятся последние тестовые зависимости junit-jupiter-engine , junit-jupiter-api и Spring :

<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.8.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.8.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.3.3</version>
<scope>test</scope>
</dependency>

Для эффективного утверждения результатов мы также будем использовать Hamcrest и путь JSON :

<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-library</artifactId>
<version>2.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.jayway.jsonpath</groupId>
<artifactId>json-path</artifactId>
<version>2.5.0</version>
<scope>test</scope>
</dependency>

3. Тестовая конфигурация Spring MVC

Теперь давайте посмотрим, как настроить и запустить тесты с поддержкой Spring.

3.1. Включить Spring в тестах с помощью JUnit 5

JUnit 5 определяет интерфейс расширения, через который классы могут интегрироваться с тестом JUnit.

Мы можем включить это расширение , добавив аннотацию @ExtendWith к нашим тестовым классам и указав класс расширения для загрузки . Для запуска теста Spring мы используем SpringExtension.class.

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

Давайте посмотрим:

@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = { ApplicationConfig.class })
@WebAppConfiguration
public class GreetControllerIntegrationTest {
....
}

Обратите внимание, что в @ContextConfiguration мы предоставили класс конфигурации ApplicationConfig.class , который загружает конфигурацию, необходимую для этого конкретного теста.

Здесь мы будем использовать класс конфигурации Java для указания конфигурации контекста. Точно так же мы можем использовать конфигурацию на основе XML:

@ContextConfiguration(locations={""})

Наконец, мы также аннотируем тест @WebAppConfiguration , который загрузит контекст веб- приложения .

По умолчанию он ищет корневое веб-приложение по пути src/main/webapp. Мы можем переопределить это местоположение, просто передав атрибут value :

@WebAppConfiguration(value = "")

3.2. Объект WebApplicationContext _

WebApplicationContext предоставляет конфигурацию веб-приложения. Он загружает все компоненты приложения и контроллеры в контекст.

Теперь мы сможем подключить контекст веб-приложения прямо к тесту:

@Autowired
private WebApplicationContext webApplicationContext;

3.3. Имитация компонентов веб-контекста

MockMvc обеспечивает поддержку тестирования Spring MVC. Он инкапсулирует все bean-компоненты веб-приложений и делает их доступными для тестирования.

Давайте посмотрим, как его использовать:

private MockMvc mockMvc;
@BeforeEach
public void setup() throws Exception {
this.mockMvc = MockMvcBuilders.webAppContextSetup(this.webApplicationContext).build();
}

Мы инициализируем объект mockMvc в аннотированном методе @BeforeEach , чтобы нам не приходилось инициализировать его внутри каждого теста.

3.4. Проверка тестовой конфигурации

Давайте проверим, правильно ли мы загружаем объект WebApplicationContext ( webApplicationContext ). Мы также проверим, подключен ли правильный servletContext :

@Test
public void givenWac_whenServletContext_thenItProvidesGreetController() {
ServletContext servletContext = webApplicationContext.getServletContext();

Assert.assertNotNull(servletContext);
Assert.assertTrue(servletContext instanceof MockServletContext);
Assert.assertNotNull(webApplicationContext.getBean("greetController"));
}

Обратите внимание, что мы также проверяем наличие bean-компонента GreetController.java в веб-контексте. Это гарантирует правильную загрузку компонентов Spring. На этом настройка интеграционного теста завершена. Теперь мы увидим, как мы можем тестировать методы ресурсов, используя объект MockMvc .

4. Написание интеграционных тестов

В этом разделе мы рассмотрим основные операции, доступные через тестовую среду.

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

Фрагменты, показанные ниже, используют статический импорт из классов MockMvcRequestBuilders или MockMvcResultMatchers .

4.1. Подтвердить имя представления

Мы можем вызвать конечную точку /homePage из нашего теста как :

http://localhost:8080/spring-mvc-test/

или же

http://localhost:8080/spring-mvc-test/homePage

Во-первых, давайте посмотрим тестовый код:

@Test
public void givenHomePageURI_whenMockMVC_thenReturnsIndexJSPViewName() {
this.mockMvc.perform(get("/homePage")).andDo(print())
.andExpect(view().name("index"));
}

Давайте разберем это:

  • Метод execute () вызовет метод запроса GET, который вернет ResultActions . Используя этот результат, мы можем получить ожидаемое утверждение об ответе, таком как его содержимое, статус HTTP или заголовок.
  • andDo(print()) распечатает запрос и ответ. Это полезно для получения подробного представления в случае ошибки.
  • andExpect() будет ожидать предоставленный аргумент. В нашем случае мы ожидаем, что «индекс» будет возвращен через MockMvcResultMatchers.view().

4.2. Проверить тело ответа

Мы будем вызывать конечную точку /greet из нашего теста как:

http://localhost:8080/spring-mvc-test/greet

Ожидаемый результат будет следующим:

{
"id": 1,
"message": "Hello World!!!"
}

Посмотрим тестовый код:

@Test
public void givenGreetURI_whenMockMVC_thenVerifyResponse() {
MvcResult mvcResult = this.mockMvc.perform(get("/greet"))
.andDo(print()).andExpect(status().isOk())
.andExpect(jsonPath("$.message").value("Hello World!!!"))
.andReturn();

Assert.assertEquals("application/json;charset=UTF-8",
mvcResult.getResponse().getContentType());
}

Давайте посмотрим, что именно происходит:

  • andExpect(MockMvcResultMatchers.status().isOk()) проверит, что HTTP-статус ответа — Ok ( 200) . Это гарантирует, что запрос был успешно выполнен.
  • andExpect(MockMvcResultMatchers.jsonPath("$.message").value("Hello World!!!")) проверит соответствие содержимого ответа аргументу " Hello World!!! Здесь мы использовали jsonPath , который извлекает содержимое ответа и предоставляет запрошенное значение.
  • andReturn() вернет объект MvcResult , который используется, когда нам нужно проверить что-то, что недоступно библиотеке напрямую. В этом случае мы добавили assertEquals для соответствия типу содержимого ответа, извлеченного из объекта MvcResult .

4. 3. Отправить запрос GET с переменной пути

Мы будем вызывать конечную точку /greetWithPathVariable/{name} из нашего теста как:

http://localhost:8080/spring-mvc-test/greetWithPathVariable/John

Ожидаемый результат будет следующим:

{
"id": 1,
"message": "Hello World John!!!"
}

Посмотрим тестовый код:

@Test
public void givenGreetURIWithPathVariable_whenMockMVC_thenResponseOK() {
this.mockMvc
.perform(get("/greetWithPathVariable/{name}", "John"))
.andDo(print()).andExpect(status().isOk())

.andExpect(content().contentType("application/json;charset=UTF-8"))
.andExpect(jsonPath("$.message").value("Hello World John!!!"));
}

MockMvcRequestBuilders.get("/greetWithPathVariable/{name}", "John") отправит запрос как " /greetWithPathVariable/John.

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

4.4. Отправить запрос GET с параметрами запроса

Мы будем вызывать конечную точку /greetWithQueryVariable?name={name} из нашего теста как:

http://localhost:8080/spring-mvc-test/greetWithQueryVariable?name=John%20Doe

В этом случае ожидаемый результат будет следующим:

{
"id": 1,
"message": "Hello World John Doe!!!"
}

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

@Test
public void givenGreetURIWithQueryParameter_whenMockMVC_thenResponseOK() {
this.mockMvc.perform(get("/greetWithQueryVariable")
.param("name", "John Doe")).andDo(print()).andExpect(status().isOk())
.andExpect(content().contentType("application/json;charset=UTF-8"))
.andExpect(jsonPath("$.message").value("Hello World John Doe!!!"));
}

param("name", "John Doe") добавит параметр запроса в запрос GET . Это похоже на « /greetWithQueryVariable?name=John%20Doe.

Параметр запроса также может быть реализован с использованием стиля шаблона URI:

this.mockMvc.perform(
get("/greetWithQueryVariable?name={name}", "John Doe"));

4.5. Отправить POST-запрос

Мы будем вызывать конечную точку /greetWithPost из нашего теста как:

http://localhost:8080/spring-mvc-test/greetWithPost

Мы должны получить вывод:

{
"id": 1,
"message": "Hello World!!!"
}

И наш тестовый код:

@Test
public void givenGreetURIWithPost_whenMockMVC_thenVerifyResponse() {
this.mockMvc.perform(post("/greetWithPost")).andDo(print())
.andExpect(status().isOk()).andExpect(content()
.contentType("application/json;charset=UTF-8"))
.andExpect(jsonPath("$.message").value("Hello World!!!"));
}

MockMvcRequestBuilders.post («/greetWithPost») отправит запрос POST . Мы можем установить переменные пути и параметры запроса так же, как и раньше, тогда как данные формы можно установить только с помощью метода param() , аналогично параметрам запроса, например:

http://localhost:8080/spring-mvc-test/greetWithPostAndFormData

Тогда данные будут:

id=1;name=John%20Doe

Итак, мы должны получить:

{
"id": 1,
"message": "Hello World John Doe!!!"
}

Давайте посмотрим на наш тест:

@Test
public void givenGreetURI_whenMockMVC_thenVerifyResponse() throws Exception {
MvcResult mvcResult = this.mockMvc.perform(MockMvcRequestBuilders.get("/greet"))
.andDo(print())
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.jsonPath("$.message").value("Hello World!!!"))
.andReturn();

assertEquals("application/json;charset=UTF-8", mvcResult.getResponse().getContentType());
}

В приведенном выше фрагменте кода мы добавили два параметра: идентификатор как «1» и имя как «Джон Доу».

5. Ограничения MockMvc

MockMvc предоставляет элегантный и простой в использовании API для вызова конечных точек сети и одновременной проверки и подтверждения их ответа. Несмотря на все свои преимущества, у него есть несколько ограничений.

Прежде всего, он использует подкласс DispatcherServlet для обработки тестовых запросов. Чтобы быть более конкретным, TestDispatcherServlet отвечает за вызов контроллеров и выполнение всей знакомой магии Spring.

Класс MockMvc внутренне обертывает этот TestDispatcherServlet . Таким образом, каждый раз, когда мы отправляем запрос с помощью метода Perform () , MockMvc будет напрямую использовать базовый TestDispatcherServlet . Следовательно, реальных сетевых подключений нет, и, следовательно, мы не будем тестировать весь сетевой стек при использовании MockMvc . ``

Кроме того, поскольку Spring подготавливает поддельный контекст веб-приложения для имитации HTTP-запросов и ответов, он может не поддерживать все функции полноценного приложения Spring .

Например, эта макетная установка не поддерживает перенаправления HTTP . Сначала это может показаться не таким уж значительным. Однако Spring Boot обрабатывает некоторые ошибки, перенаправляя текущий запрос на конечную точку /error . Поэтому, если мы используем MockMvc, мы не сможем протестировать некоторые сбои API.

В качестве альтернативы MockMvc мы можем настроить более реальный контекст приложения, `а затем использовать RestTemplate или` даже REST-assured для тестирования нашего приложения.

Например, это легко сделать с помощью Spring Boot:

@SpringBootTest(webEnvironment = DEFINED_PORT)
public class GreetControllerRealIntegrationTest {

@Before
public void setUp() {
RestAssured.port = DEFAULT_PORT;
}

@Test
public void givenGreetURI_whenSendingReq_thenVerifyResponse() {
given().get("/greet")
.then()
.statusCode(200);
}
}

Здесь нам даже не нужно добавлять @ExtendWith(SpringExtension.class) .

Таким образом, каждый тест будет делать настоящий HTTP-запрос к приложению, которое прослушивает случайный TCP-порт.

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

В этой статье мы реализовали несколько простых интеграционных тестов с поддержкой Spring.

Мы также рассмотрели создание объектов WebApplicationContext и MockMvc , которые играют важную роль в вызове конечных точек приложения.

В дальнейшем мы обсудили, как отправлять запросы GET и POST с вариантами передачи параметров и как проверять статус, заголовок и содержимое HTTP-ответа.

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

Наконец, реализация всех этих примеров и фрагментов кода доступна на GitHub .