Тесты контроллеров: MockMvc
Тест сервиса не проверит, что контроллер отвечает правильным кодом, что JSON содержит нужные поля и что гость не может создать статью. Для этого нужно отправлять HTTP-запросы. MockMvc делает это без запуска Tomcat: запрос проходит через все фильтры безопасности, контроллер и обработчики ошибок, а ответ можно проверить в коде.
Зависимость для безопасности
Чтобы в тестах «входить» под нужным пользователем, добавьте:
<!-- pom.xml -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security-test</artifactId>
<scope>test</scope>
</dependency>Тестовый класс
// src/test/java/com/example/blog/PostControllerTests.java
package com.example.blog;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
import org.springframework.http.MediaType;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.transaction.annotation.Transactional;
@SpringBootTest
@AutoConfigureMockMvc
@ActiveProfiles("test")
@Transactional
class PostControllerTests {
@Autowired
private MockMvc mvc;
@Autowired
private UserRepository users;
@BeforeEach
void createUser() {
users.save(new User("anna", "{noop}changeme"));
}
@Test
void returnsNotFoundForMissingPost() throws Exception {
mvc.perform(get("/api/posts/999"))
.andExpect(status().isNotFound())
.andExpect(jsonPath("$.detail").value("Статья 999 не найдена"));
}
@Test
void rejectsAnonymousPost() throws Exception {
mvc.perform(post("/api/posts")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{"title": "Привет", "body": "Текст"}
"""))
.andExpect(status().isUnauthorized());
}
@Test
@WithMockUser(username = "anna")
void createsPost() throws Exception {
mvc.perform(post("/api/posts")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{"title": "Привет", "body": "Текст"}
"""))
.andExpect(status().isCreated())
.andExpect(header().exists("Location"))
.andExpect(jsonPath("$.title").value("Привет"))
.andExpect(jsonPath("$.author").value("anna"));
}
}@AutoConfigureMockMvc создаёт бин MockMvc. В Spring Boot 4 эта аннотация лежит в пакете org.springframework.boot.webmvc.test.autoconfigure. perform отправляет запрос, andExpect проверяет ответ. jsonPath("$.title") достаёт поле из JSON: $ — корень ответа.
Вход в тесте
@WithMockUser(username = "anna") выполняет тест так, будто Анна уже вошла. Пароль не проверяется, но PostService ищет автора в базе — поэтому в @BeforeEach мы создаём настоящего пользователя anna.
Формы и CSRF
У страниц сайта CSRF включён, поэтому запрос формы должен нести токен. В тесте его добавляет with(csrf()):
// src/test/java/com/example/blog/PageControllerTests.java
@Test
@WithMockUser(username = "anna")
void createsPostFromForm() throws Exception {
mvc.perform(post("/posts").param("title", "Привет").param("body", "Текст").with(csrf()))
.andExpect(status().is3xxRedirection())
.andExpect(redirectedUrlPattern("/posts/*"));
}csrf() импортируется из SecurityMockMvcRequestPostProcessors. param передаёт поля формы, а проверка ожидает переадресацию на страницу новой статьи.
Результат
[INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 2.368 s -- in com.example.blog.PageControllerTests
[INFO] Tests run: 4, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.254 s -- in com.example.blog.PostControllerTestsЧастая ошибка
Без .with(csrf()) тот же запрос формы получит 403, даже под @WithMockUser: фильтр CSRF отклоняет запрос раньше, чем дело дойдёт до входа и контроллера. Если тест формы неожиданно падает с Status expected:<302> but was:<403>, первым делом проверьте токен. Для адресов /api/** он не нужен — там CSRF отключён.