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 .