Spring Boot Test — slice-тесты Controller
Цель урока
Сделать полноценный CRUD для задач с правильными HTTP-статусами (201/204/404), обработкой ошибок, и подготовить Task к подключению JPA.
Теория · для собеса
1 Slice-тесты ▸
Slice-тест = поднимает только нужный «слой» Spring, не весь контекст.
@WebMvcTest(TaskController.class)— только Controller-слой@DataJpaTest— только JPA (с in-memory БД)@RestClientTest— только RestTemplate / WebClient
2 MockMvc ▸
MockMvc — имитация HTTP-запросов без реального сервера. Быстро, контролируемо.
mockMvc.perform(get("/api/tasks"))
.andExpect(status().isOk())
.andExpect(jsonPath("$[0].title").value("Task 1"));
3
@WebMvcTest
▸
- Загружает только Spring MVC инфраструктуру
@MockBeanдля подсовывания моков сервисов@Autowired MockMvc— для выполнения запросов- Валидация (
@Valid) работает - Security — НЕ загружается (если хотим — добавляем
@Import(SecurityConfig.class))
---
Практика: улучшаем CRUD
-
1
test/.../controller/TaskControllerTest.javapackage com.taskflow.controller; import com.fasterxml.jackson.databind.ObjectMapper; import com.taskflow.dto.CreateTaskRequest; import com.taskflow.dto.TaskResponse; import com.taskflow.dto.UpdateTaskRequest; import com.taskflow.exception.ResourceNotFoundException; import com.taskflow.service.TaskService; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; import java.time.Instant; import java.util.List; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.*; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; @WebMvcTest(TaskController.class) class TaskControllerTest { @Autowired private MockMvc mockMvc; @Autowired private ObjectMapper objectMapper; @MockBean private TaskService taskService; private TaskResponse sampleResponse(Long id, String title) { return new TaskResponse(id, title, "desc", false, 1L, "alice", Instant.now(), Instant.now()); } @Test void shouldReturnListOfTasks() throws Exception { when(taskService.findAll()).thenReturn(List.of( sampleResponse(1L, "Task 1"), sampleResponse(2L, "Task 2") )); mockMvc.perform(get("/api/tasks")) .andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON)) .andExpect(jsonPath("$").isArray()) .andExpect(jsonPath("$.length()").value(2)) .andExpect(jsonPath("$[0].id").value(1)) .andExpect(jsonPath("$[0].title").value("Task 1")) .andExpect(jsonPath("$[1].title").value("Task 2")); } @Test void shouldReturnTaskWhenExists() throws Exception { when(taskService.findById(1L)).thenReturn(sampleResponse(1L, "Task 1")); mockMvc.perform(get("/api/tasks/{id}", 1L)) .andExpect(status().isOk()) .andExpect(jsonPath("$.id").value(1)) .andExpect(jsonPath("$.title").value("Task 1")); } @Test void shouldReturn404WhenNotFound() throws Exception { when(taskService.findById(99L)) .thenThrow(new ResourceNotFoundException("Task", 99L)); mockMvc.perform(get("/api/tasks/{id}", 99L)) .andExpect(status().isNotFound()) .andExpect(jsonPath("$.status").value(404)) .andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString("99"))); } @Test void shouldCreateTaskAndReturn201() throws Exception { CreateTaskRequest req = new CreateTaskRequest("New task", "Description"); when(taskService.create(any(CreateTaskRequest.class))) .thenReturn(sampleResponse(1L, "New task")); mockMvc.perform(post("/api/tasks") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(req))) .andExpect(status().isCreated()) .andExpect(header().exists("Location")) .andExpect(header().string("Location", org.hamcrest.Matchers.containsString("/api/tasks/1"))) .andExpect(jsonPath("$.id").value(1)) .andExpect(jsonPath("$.title").value("New task")); verify(taskService).create(any(CreateTaskRequest.class)); } @Test void shouldReturn400WhenTitleIsBlank() throws Exception { CreateTaskRequest req = new CreateTaskRequest("", "Desc"); // title пустой mockMvc.perform(post("/api/tasks") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(req))) .andExpect(status().isBadRequest()) .andExpect(jsonPath("$.status").value(400)) .andExpect(jsonPath("$.fieldErrors").isArray()) .andExpect(jsonPath("$.fieldErrors[0].field").value("title")); verify(taskService, never()).create(any()); } @Test void shouldUpdateTask() throws Exception { UpdateTaskRequest req = new UpdateTaskRequest("Updated", "New desc", true); when(taskService.update(eq(1L), any(UpdateTaskRequest.class))) .thenReturn(sampleResponse(1L, "Updated")); mockMvc.perform(put("/api/tasks/{id}", 1L) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(req))) .andExpect(status().isOk()) .andExpect(jsonPath("$.title").value("Updated")); } @Test void shouldDeleteTaskAndReturn204() throws Exception { doNothing().when(taskService).delete(1L); mockMvc.perform(delete("/api/tasks/{id}", 1L)) .andExpect(status().isNoContent()); verify(taskService).delete(1L); } @Test void shouldReturn404WhenDeletingNonExisting() throws Exception { org.mockito.Mockito.doThrow(new ResourceNotFoundException("Task", 99L)) .when(taskService).delete(99L); mockMvc.perform(delete("/api/tasks/{id}", 99L)) .andExpect(status().isNotFound()); } } -
2Тест валидации
titleс пустой строкой → 400, не доходит до сервиса. -
3Тест с Security
Если хотим тестировать с авторизацией:
@WebMvcTest(TaskController.class) @Import(SecurityConfig.class) class TaskControllerSecurityTest { @Autowired private MockMvc mockMvc; @MockBean private TaskService taskService; @MockBean private JwtTokenProvider jwtTokenProvider; @MockBean private CustomUserDetailsService userDetailsService; @Test void shouldReturn401WithoutToken() throws Exception { mockMvc.perform(get("/api/tasks")) .andExpect(status().isUnauthorized()); } @Test @WithMockUser(username = "alice", roles = "USER") void shouldReturnTasksForAuthenticatedUser() throws Exception { when(taskService.findAll()).thenReturn(List.of()); mockMvc.perform(get("/api/tasks")) .andExpect(status().isOk()); } }---
Зачем это на собесе
После урока ты должен уметь ответить на:
@Valid работает, и фронт получит 400 + список ошибок.TaskResponse? Тест сразу покажет, что сломалось.@WebMvcTest загружает ~10 бинов, не 100.5 вопросов на углубление
Раскрой вопрос и нажми «🤔 Хочу разобрать подробнее» — он попадёт в страницу ответов.
Разбор внутри: лёгкий slice controller-а и полный контекст приложения.
Разбор внутри: переход на @MockitoBean и почему старое имя путает.
Разбор внутри: MockMultipartFile без реального файла на диске.
Разбор внутри: загрузка контекста и обязательные зависимости.
Разбор внутри: component scan, @Import и тесты ошибок API.
Готов идти дальше?
Выбери вопросы, которые тебе интересны, и изучи их. Потом — к следующему уроку.
🚀 Перейти к Уроку 19Сначала пройди все секции и выбери хотя бы 1 вопрос
⬅️ Назад к Уроку 17