@WebMvcTest Test 내에서만 사용하는 TestController를 등록하고, 사용하는 법? → @Import / @TestConfiguration / 따로 파일 설정 방식이 있고, 그 중 @TestConfiguration 이게 더 이해하기 좋아 보여서 이걸 택함.
GlobalExceptionHandlerTest에서만 사용하는 TestController는 어떤 식으로 등록하면 좋을까?
참고:
@WebMvcTest(controllers = GlobalExceptionHandlerTest.TestController.class) 처럼 WebMvcTest에는 실제 테스트할 controller를 입력한다.
→ 지정한 컨트롤러 클래스만 로딩해서 슬라이스 테스트함. → 즉, 지정된 컨트롤러와 관련된 MVC 빈(@Controller, @ControllerAdvice, Jackson, MockMvc 등)만 등록됨.
@Import는 명시적으로 빈 등록하는 법.
package today.sesac.versebyverse.global.advice;
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.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Email;
import java.util.Locale;
import org.junit.jupiter.api.DisplayName;
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.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.result.MockMvcResultHandlers;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import today.sesac.versebyverse.global.advice.GlobalExceptionHandlerTest.TestController;
import today.sesac.versebyverse.global.config.MessageConfig;
import today.sesac.versebyverse.global.config.TestSecurityConfig;
import today.sesac.versebyverse.global.exception.LoginRequiredException;
import today.sesac.versebyverse.global.exception.PermissionRequiredException;
/**
* 전역 예외 처리 테스트.
*/
@Import({TestController.class, GlobalExceptionHandler.class, TestSecurityConfig.class,
MessageConfig.class})
//TODO: 테스트 커스텀 유저 추가 애노테이션 구현 필요
@WebMvcTest(controllers = GlobalExceptionHandlerTest.TestController.class)
class GlobalExceptionHandlerTest {
@Autowired
private MockMvc mockMvc;
@Test
@DisplayName("[예외]-MethodArgumentNotValidException 처리 테스트")
void handleMethodArgumentNotValidException() throws Exception {
ObjectMapper objectMapper = new ObjectMapper();
String invalidJson = objectMapper.writeValueAsString(new TestReq("invalidEmailFormat"));
mockMvc.perform(post("/test/valid")
.contentType(MediaType.APPLICATION_JSON)
.content(invalidJson))
.andExpect(status().isBadRequest())
.andExpect(jsonPath(IS_SUCCESS).value(false))
.andExpect(jsonPath("$.error[0].name").value("INVALID_REQUEST"))
.andExpect(jsonPath("$.error[0].message").value("유효하지 않은 형식입니다. 요청 값을 다시 확인해 주세요."))
.andExpect(jsonPath("$.error[0].param").value("email"))
.andDo(MockMvcResultHandlers.print());
}
/**
* 테스트용 컨트롤러.
*/
@RestController
static class TestController {
@GetMapping("/test/error/runtime")
public void testEndpoint() {
throw new RuntimeException("Test Runtime Exception");
}
@GetMapping("/test/error/login-required")
public void loginRequiredEndpoint() {
throw new LoginRequiredException("member", "로그인이 필요합니다. - stack trace에 기록된 메시지");
}
@GetMapping("/test/no-permission")
public void noPermissionEndpoint() {
throw new PermissionRequiredException("member", "권한이 없습니다.");
}
@PostMapping("/test/valid")
public void validTest(@Valid @RequestBody TestReq request) {
// 요청이 유효하면 아무 일도 하지 않음
}
}
/**
* 테스트용 요청 DTO.
*
* @param email 이메일
*/
record TestReq(
@Email(message = "유효하지 않은 형식입니다. 요청 값을 다시 확인해 주세요.")
String email
) {
}
//
// /**
// * 테스트용 컨트롤러를 빈으로 등록하는 설정 클래스.
// */
// @TestConfiguration
// static class TestConfig {
//
// @Bean
// public TestController testController() {
//
// return new TestController();
// }
// }
}
문제
@WebMvcTest의 controllers 속성은 스프링 빈으로 등록된 컨트롤러만 대상으로 스캔해 줌. 하지만 지금 TestController는 내부 static class라서, @SpringBootApplication 컴포넌트 스캔 경로에 포함되지 않음.
해결
@Import에서 TestController 를 빈으로 등록해 가져오기.@TestConfiguration을 사용해 직접 Bean으로 등록하기.@WebMvcTest(controllers = TestController.class) 이렇게 사용하기.@Import가 깔끔할 것 같지만, 명시적으로 보이는 게 현 상황에서는 더 나을 것 같아서 @TestConfiguration을 사용해 직접 Bean으로 등록하는 방법을 택함.