Тестирование REST-контроллеров Spring: проверка ответов с MockMvc
Для кого эта статья:
- Разработчики, работающие с Spring Framework и разрабатывающие веб-приложения
- Специалисты по тестированию, занимающиеся проверкой API
Студенты и начинающие программисты, интересующиеся тестированием на Java и Spring
Если вы когда-либо разрабатывали веб-приложение на Spring Framework, то понимаете, насколько критично убедиться, что ваши REST-контроллеры корректно обрабатывают запросы и возвращают ожидаемые ответы. Пропущенная запятая в JSON или неверная структура XML могут превратиться в часы отладки и разочарованных пользователей. MockMvc — это мощный инструмент для тестирования Spring MVC контроллеров, который позволяет без запуска полноценного сервера проверить, содержит ли ответ именно то, что мы ожидаем. 🧪 Давайте разберемся, как правильно проверять содержимое ответа и убедиться, что наш код работает как часы!
Не тратьте время на изобретение велосипеда! На Курсе Java-разработки от Skypro вы освоите не только основы тестирования с MockMvc, но и продвинутые техники проверки API-ответов, которые используются в реальных проектах. Под руководством практикующих разработчиков вы научитесь писать надёжные тесты, которые защитят ваш код от регрессий и ускорят разработку в 2-3 раза.
MockMvc: тестирование контроллеров Spring приложений
MockMvc — это фреймворк, который позволяет тестировать Spring MVC контроллеры без необходимости запуска полноценного HTTP-сервера. Это значительно ускоряет выполнение тестов и делает их более изолированными от внешних зависимостей.
Для начала работы с MockMvc необходимо настроить тестовое окружение. Вот типичный пример инициализации MockMvc в тестовом классе:
@SpringBootTest
@AutoConfigureMockMvc
public class UserControllerTest {
@Autowired
private MockMvc mockMvc;
@Test
public void testGetUserDetails() throws Exception {
mockMvc.perform(get("/api/users/1"))
.andExpect(status().isOk())
.andExpect(content().contentType(MediaType.APPLICATION_JSON));
}
}
Что делает MockMvc особенно полезным:
- Запуск запросов к контроллерам без реального HTTP-сервера
- Полный контроль над параметрами запроса (заголовки, тело, параметры)
- Широкие возможности проверки ответов с помощью матчеров
- Возможность тестирования контроллеров в изоляции от других компонентов
Алексей, Lead Java Developer
В моей команде был случай, когда один из разработчиков написал новый REST-эндпоинт, который должен был вернуть статус подписки пользователя. Тесты проходили, код прошёл ревью, но в продакшене клиенты начали жаловаться на ошибки. Оказалось, что контроллер возвращал строку "ACTIVE" вместо "SUBSCRIPTION_ACTIVE", как ожидалось клиентской частью.
Проблему быстро обнаружили и исправили, но после этого инцидента мы внедрили строгую политику: любой REST-контроллер должен иметь тесты, проверяющие точное соответствие возвращаемых строк в ответе. Использование MockMvc с явными проверками содержимого ответа сократило количество подобных ошибок практически до нуля.
При настройке MockMvc важно понимать два основных подхода:
| Подход | Описание | Использование |
|---|---|---|
| Standalone setup | Создание MockMvc без загрузки Spring контекста |
|
| WebApplicationContext | Использование полного Spring контекста |
|
Выбор подхода зависит от целей тестирования. Standalone подход быстрее и подходит для изолированных юнит-тестов, тогда как WebApplicationContext нужен для полноценных интеграционных тестов, учитывающих все аспекты Spring MVC.

Основные методы проверки ответа в MockMvc тестах
После выполнения запроса с помощью MockMvc, нам нужно проверить полученный ответ. Spring предоставляет множество удобных методов для проверки различных аспектов HTTP-ответа.
Основные проверки структурированы следующим образом:
- status() — проверка HTTP статуса ответа
- header() — проверка HTTP заголовков
- content() — проверка тела ответа
- jsonPath() — проверка содержимого JSON с использованием выражений
- xpath() — проверка содержимого XML с использованием XPath
- cookie() — проверка куки в ответе
Рассмотрим базовый пример использования этих методов:
@Test
public void testUserCreation() throws Exception {
mockMvc.perform(post("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"name\":\"John\",\"email\":\"john@example.com\"}"))
.andExpect(status().isCreated())
.andExpect(header().string("Location", containsString("/api/users/")))
.andExpect(content().string(containsString("User created successfully")))
.andExpect(jsonPath("$.id").exists());
}
Важно понимать, что MockMvc предлагает различные способы проверки содержимого в зависимости от типа данных. Выбор подходящего метода значительно улучшает читаемость и надёжность тестов. 🔍
| Метод проверки | Тип контента | Пример | Когда использовать |
|---|---|---|---|
| content().string() | Любой |
| Для простых ответов, где требуется точное совпадение |
| content().string(containsString()) | Любой |
| Когда нужно проверить наличие подстроки |
| jsonPath() | JSON |
| Для проверки JSON структур |
| xpath() | XML |
| Для проверки XML ответов |
Существуют также полезные комбинации методов для более сложных проверок:
// Проверка JSON массива определённой длины
.andExpect(jsonPath("$.users", hasSize(3)))
// Проверка конкретных значений в JSON
.andExpect(jsonPath("$.users[0].name").value("John"))
// Комбинирование нескольких проверок
.andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON))
.andExpect(content().string(containsString("success")))
Проверка точного соответствия строки в HTTP ответе
Когда требуется убедиться, что тело ответа содержит точно определенный текст, метод content().string() — ваш лучший выбор. Этот метод сравнивает тело ответа с ожидаемой строкой побайтово, что гарантирует полное соответствие.
Вот пример проверки точного соответствия строки в ответе:
@Test
public void testExactStringMatch() throws Exception {
mockMvc.perform(get("/api/message"))
.andExpect(status().isOk())
.andExpect(content().string("Hello, World!"));
}
Важно понимать, что этот метод проверяет абсолютно точное соответствие, включая пробелы, знаки препинания и регистр символов. Если хотя бы один символ отличается, тест не пройдет. 🔍
Для более сложных случаев вы можете использовать матчеры Hamcrest, которые дают больше гибкости:
// Игнорирование регистра
.andExpect(content().string(equalToIgnoringCase("hello, world!")))
// Игнорирование пробелов
.andExpect(content().string(equalToCompressingWhiteSpace("Hello, World!")))
Если у вас есть предварительно подготовленный эталонный файл с ожидаемым ответом, можно загрузить его и использовать для сравнения:
@Test
public void testResponseAgainstFile() throws Exception {
String expectedResponse = new String(
Files.readAllBytes(Paths.get("src/test/resources/expected-response.txt"))
);
mockMvc.perform(get("/api/complex-data"))
.andExpect(status().isOk())
.andExpect(content().string(expectedResponse));
}
Проверка точного соответствия особенно важна в следующих случаях:
- При возврате строго форматированных данных (например, XML или HTML шаблонов)
- При тестировании эндпоинтов, которые должны возвращать конкретные сообщения
- При работе с контрактным тестированием, где формат ответа строго определен
Марина, QA Engineer
Однажды нам пришлось разбираться со странной проблемой: клиентское приложение перестало корректно работать после, казалось бы, безобидного обновления API. Тесты проходили успешно, но пользователи сообщали об ошибках.
Углубившись в проблему, мы обнаружили, что разработчик изменил формат ответа с "true" на "TRUE". Наши тесты использовали проверку jsonPath().exists(), что не позволило выявить проблему с регистром. После этого мы внедрили более строгие проверки с content().string() для критичных частей API.
Теперь мы следуем простому правилу: если формат ответа принципиален для работы клиента — используем точное сравнение, а не приблизительные проверки. Это спасло нас от множества подобных проблем.
Поиск подстроки в теле ответа Spring контроллера
В реальных проектах часто бывает достаточно проверить, что ответ содержит определенную подстроку, а не соответствует ей полностью. Для этого MockMvc предоставляет элегантное решение с использованием матчеров Hamcrest.
Основной способ проверки наличия подстроки в ответе:
@Test
public void testSubstringInResponse() throws Exception {
mockMvc.perform(get("/api/users/summary"))
.andExpect(status().isOk())
.andExpect(content().string(containsString("Total users: 42")));
}
Можно комбинировать несколько проверок, чтобы убедиться, что ответ содержит все необходимые элементы:
@Test
public void testMultipleSubstrings() throws Exception {
mockMvc.perform(get("/api/dashboard"))
.andExpect(status().isOk())
.andExpect(content().string(allOf(
containsString("User count"),
containsString("Active sessions"),
containsString("Server status")
)));
}
Иногда бывает необходимо убедиться, что ответ не содержит определенную подстроку (например, конфиденциальные данные):
@Test
public void testResponseDoesNotContainSensitiveData() throws Exception {
mockMvc.perform(get("/api/public/user/1"))
.andExpect(status().isOk())
.andExpect(content().string(not(containsString("password"))))
.andExpect(content().string(not(containsString("SSN"))));
}
Для более сложных случаев можно использовать регулярные выражения:
@Test
public void testResponsePattern() throws Exception {
mockMvc.perform(get("/api/generate-token"))
.andExpect(status().isOk())
.andExpect(content().string(matchesPattern("\\{\"token\":\"[A-Za-z0-9-_]{43}\"\\}")));
}
Что особенно полезно при проверке подстроки в больших ответах:
- Ищите именно те части, которые критичны для функциональности
- Используйте несколько небольших проверок вместо одного большого сравнения
- Помните о возможных изменениях в форматировании или незначительных изменениях в ответе
Для упрощения отладки в случае ошибок рекомендуется выводить фактический ответ:
@Test
public void testWithResponsePrint() throws Exception {
mockMvc.perform(get("/api/complex-data"))
.andDo(print()) // Выведет полный запрос и ответ в лог
.andExpect(status().isOk())
.andExpect(content().string(containsString("expected fragment")));
}
Работа с JSON и XML содержимым через MockMvc матчеры
Когда дело касается структурированных данных, таких как JSON или XML, использование простых строковых проверок становится неэффективным. Для этого Spring предоставляет специализированные матчеры, которые значительно упрощают работу со сложными структурами данных. 📊
Для работы с JSON используется jsonPath — мощный инструмент, основанный на языке выражений JsonPath:
@Test
public void testJsonResponse() throws Exception {
mockMvc.perform(get("/api/users/1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.id").value(1))
.andExpect(jsonPath("$.name").value("John Doe"))
.andExpect(jsonPath("$.email").value("john@example.com"))
.andExpect(jsonPath("$.roles", hasSize(2)))
.andExpect(jsonPath("$.roles[0]").value("USER"))
.andExpect(jsonPath("$.roles[1]").value("ADMIN"));
}
Для XML документов используется XPath — стандарт для навигации по XML структурам:
@Test
public void testXmlResponse() throws Exception {
mockMvc.perform(get("/api/products/xml"))
.andExpect(status().isOk())
.andExpect(xpath("/products/product").nodeCount(3))
.andExpect(xpath("/products/product[1]/name").string("Laptop"))
.andExpect(xpath("/products/product[1]/price").number(1299.99));
}
Преимущества использования специализированных матчеров:
- Более высокая устойчивость к изменениям в форматировании
- Лучшая читаемость тестов — видно, какие именно поля проверяются
- Более информативные сообщения об ошибках при сбоях тестов
- Возможность проверить только нужные части структуры, игнорируя остальное
Рассмотрим более сложные случаи использования JsonPath с различными операторами:
| JsonPath выражение | Описание | Пример использования |
|---|---|---|
$.store.book[*].author | Все авторы всех книг |
|
| $.store.book[?(@.price < 10)] | Все книги дешевле 10 |
|
| $.store.book[-1:] | Последняя книга |
|
| $.store.book[0,1].title | Названия первых двух книг |
|
Проверка сложных структур с помощью комбинаций матчеров:
@Test
public void testComplexJsonStructure() throws Exception {
mockMvc.perform(get("/api/departments"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.departments").isArray())
.andExpect(jsonPath("$.departments[*].employees").exists())
// Проверка, что в каждом отделе есть хотя бы один сотрудник с зарплатой > 50000
.andExpect(jsonPath("$.departments[*].employees[?(@.salary > 50000)]").exists())
// Проверка суммарного количества сотрудников
.andExpect(jsonPath("$.totalEmployees").value(greaterThan(10)));
}
Для работы с XML также можно использовать пространства имен, что часто встречается в SOAP-сервисах:
@Test
public void testSoapXmlResponse() throws Exception {
Map<String, String> namespaces = new HashMap<>();
namespaces.put("soap", "http://schemas.xmlsoap.org/soap/envelope/");
namespaces.put("m", "http://example.org/stock");
mockMvc.perform(post("/api/soap-service")
.contentType(MediaType.TEXT_XML)
.content("<soap:Envelope>...</soap:Envelope>"))
.andExpect(status().isOk())
.andExpect(xpath("/soap:Envelope/soap:Body/m:GetStockPriceResponse/m:Price", namespaces)
.number(greaterThan(100.0)));
}
При тестировании REST API рекомендуется уделять особое внимание не только структуре ответа, но и контрактам API. Комбинирование различных подходов проверки — от простых строковых до сложных JsonPath и XPath выражений — позволяет создать надежные тесты, устойчивые к рефакторингу и расширению функциональности. Хорошие тесты не только ловят ошибки, но и служат живой документацией к вашему API, помогая новым членам команды быстрее понять его работу.