[WebMVC 구조]

이번 시간에는 WebMVC 구조의 하나인 Controller 부분에 대한 테스트 코드를 작성하는 방법에 대해 알아보자.
컨트롤러는 사용자 입력을 받아 비즈니스 로직을 호출하고, 그 결과를 사용자에게 반환하는 역할을 한다.
컨트롤러 테스트는 웹 애플리케이션에서 사용자가 요청을 보낼 때 이를 처리하는 컨트롤러 레이어가 올바르게 작동하는지 검증하는 테스트이다.
(컨트롤러가 예상대로 작동하는지, 웹 요청과 응답이 제대로 이루어지는지를 검증한다.)
package org.example.expert.domain.comment.service.controller;
import org.example.expert.domain.comment.controller.CommentController;
import org.example.expert.domain.comment.service.CommentService;
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.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.ResultActions;
import java.util.List;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.BDDMockito.given;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@WebMvcTest(CommentController.class) // 컨트롤러 테스트 환경 지정
public class CommentControllerTest {
@Autowired
private MockMvc mockMvc;
@MockitoBean // 외부 의존성에 영향을 받지 않도록 기존 스프링 컨텍스트의 Bean을 Mock 객체로 교체(스프링 부트 3.4.0 버전부터 @MockBean에서 @MockitoBean으로 대체)
private CommentService commentService;
@Test
public void 댓글_조회() throws Exception {
// given
long todoId = 1L;
given(commentService.getComments(anyLong())).willReturn(List.of());
// when
ResultActions resultActions = mockMvc.perform(get("/todos/{todoId}/comments", todoId));
// then
resultActions.andExpect(status().isOk());
}
}
@WebMvcTest(테스트할 컨트롤러.class) : 해당 컨트롤러만 테스트할 환경으로 설정한다.
@ExtendWith(MockitoExtension.class) vs @WebMvcTest
※ @ExtendWith(MockitoExtension.class) : 의존성 주입을 위해 객체를 직접 가짜인 Mock 객체로 초기화하고 특정 객체 테스트를 진행한다.
※ @WebMvcTest : 스프링 MVC 웹 계층의 컨트롤러를 단위 테스트할 때 사용하는 어노테이션이다. 실제 애플리케이션 서버를 배포하지 않고도 웹 계층의 컨트롤러 동작을 독립적으로 테스트할 수 있도록 필요한 빈들만 로드하여 테스트 속도를 향상시킨다. 이 어노테이션을 사용하면 MockMvc가 자동으로 스프링 컨테이너에 빈으로 등록되기 때문에, 필드에 @Autowired만 해주면 바로 사용할 수 있다.
(※ 스프링 부트 3.4.0 버전부터 MockBean에서 MockitoBean으로 대체되었습니다.)
@MockitoBean: 외부 의존성에 영향을 받지 않도록 Spring 컨텍스트에서 관리되는 Bean을 mock 객체로 교체한다.
@Mock vs @MockitoBean
※ @Mock : 단위 테스트에서 사용되며 Mock 객체를 직접 생성하여 사용하기 때문에 스프링 컨텍스트(컨테이너)와 관련이 없다.
※ @MockitoBean : 통합/슬라이스 테스트 (@SpringBootTest, @WebMvcTest) 테스트에서 사용되고 Mock 객체를 생성하고, 스프링 컨텍스트에 등록하여 관련된 객체와 연결하여 통합 테스트를 수행할 수 있다.
➜ 통합 테스트에서 컨테이너가 필요하기 때문에 @MockBean을 통해 빈을 등록하고, 단위 테스트는 컨테이너가 필요없기 때문에 @Mock을 통해 각 객체를 생성하면 된다.

@Mock → 스프링 없이 테스트할 때
@MockBean → 스프링 테스트에서 실제 빈을 Mock으로 대체할 때
MockMvc : HTTP 요청을 모의(Mock)하여 실제 서블릿 컨테이너를 사용하지 않고도 웹 애플리케이션의 컨트롤러를 테스트할 수 있게 해준다.
MockMvc를 사용하여 다양한 HTTP 요청을 시뮬레이션하고 응답을 검증할 수 있다.
(MockMvc를 이용한 Get 요청 예시)
// when & then
mockMvc.perform(get("/movies/{id}", movieId)) // GET 요청 시뮬레이션
.andExpect(status().isOk()) // 응답 상태 코드가 200인지 확인
.andExpect(jsonPath("$.title").value("Inception")) // 반환된 JSON의 title 값 검증
.andExpect(jsonPath("$.genre").value("Sci-Fi")) // 반환된 JSON의 genre 값 검증
.andDo(print()); // 콘솔 창 출력
(MockMvc를 이용한 Post 요청 예시)
// when & then
mockMvc.perform(post("/movies")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(movie))) // JSON 변환 후 요청 본문에 포함
.andExpect(status().isCreated()) // 응답 상태 코드가 201인지 확인
.andExpect(jsonPath("$.id").value(1)) // 반환된 JSON의 id 값 검증
.andExpect(jsonPath("$.title").value("Inception")) // 반환된 JSON의 title 값 검증
.andExpect(jsonPath("$.genre").value("Sci-Fi")) // 반환된 JSON의 genre 값 검증
.andDo(print()); // 콘솔 창 출력
※ mockMvc.의 결과값은 ResultActions이다.