[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; // MockMvc를 사용하여 HTTP 요청을 흉내
@MockBean // 외부 의존성에 영향을 받지 않도록 기존 스프링 컨텍스트의 Bean을 Mock 객체로 교체
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를 위한 테스트로 컨트롤러와 관련된 애들만 빈으로 등록하고, Service, Repository와 같은 레이어는 빈으로 등록하지 않아 의존성을 제거한다.
즉, 스프링 컨텍스트에서 컨트롤러와 관련된 애들만 빈으로 등록하고 테스트한다.
(@WebMvcTest는 빈으로 등록해서 사용하는 통합 테스트의 일종이다.)
@MockBean
: 외부 의존성에 영향을 받지 않도록 Spring 컨텍스트에서 관리되는 Bean을 mock 객체로 교체한다.
@Mock vs @MockBean
※ @Mock
: 단위 테스트에서 사용되며 Mock 객체를 직접 생성하여 사용하기 때문에 스프링 컨텍스트(컨테이너)와 관련이 없다.
※ @MockBean
: 통합 테스트에서 사용되고 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
이다.