인증 로직을 구현하기 전 통과 했던 테스트 코드가 인증 로직을 추가한 이후 통과하지 않는 문제가 발생했습니다.
문제의 테스트 코드는 다음과 같았습니다:
@WebMvcTest(EventController.class)
@Import(SecurityTestConfig.class)
@AutoConfigureMockMvc(addFilters = false)
@WithMockCustomUser
class EventControllerTest {
...
@Test
@DisplayName("존재하지 않는 행사 ID로 조회 시 404 응답을 반환한다")
void getEventDetail_WithNonExistingId_ReturnsNotFound() throws Exception {
// given
Long nonExistingId = 999L;
when(eventService.getEventDetail(nonExistingId))
.thenThrow(new EventNotFoundException());
// when & then
mockMvc.perform(get("/events/{eventId}", nonExistingId)
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isNotFound())
.andExpect(jsonPath("$.code").value(EventErrorCode.EVENT_NOT_FOUND.getCode()))
.andExpect(jsonPath("$.message").value(EventErrorCode.EVENT_NOT_FOUND.getMessage()));
}
...
}
MockHttpServletResponse:
Status = 200
Error message = null
Headers = []
Content type = null
Body =
예상했던 404 대신 200 OK가 반환되었고, 응답 본문도 비어있었습니다. 당연히 테스트는 실패했습니다. 하지만 실제 애플리케이션에서는 정상적으로 404가 반환됐습니다.
원인은 컨트롤러에서 인증 상태에 따라 다른 서비스 메서드를 호출하는데 테스트 코드에서 잘못된 서비스 메서드를 모킹했기 때문이었습니다.
컨트롤러에선 인증되지 않은 유저의 경우 getEventDetail
을 호출하고 인증된 유저의 경우 getEventDetailForUser
를 호출합니다:
@GetMapping("/{eventId}")
public EventDetailResponse getEventDetail(
@PathVariable Long eventId,
@AuthenticationPrincipal CustomUserDetails userDetails) {
if (userDetails == null) {
return eventService.getEventDetail(eventId);
}
return eventService.getEventDetailForUser(userDetails.getUser(), eventId);
}
그런데 테스트 클래스에선 @WithMockCustomUser
로 인증된 사용자를 사용하고 있는데 잘못된 서비스 메서드를 모킹하고 있었습니다.
@WebMvcTest(EventController.class)
@Import(SecurityTestConfig.class)
@AutoConfigureMockMvc(addFilters = false)
@WithMockCustomUser
class EventControllerTest {
// ...
}
@Test
@DisplayName("존재하지 않는 행사 ID로 조회 시 404 응답을 반환한다")
void getEventDetail_WithNonExistingId_ReturnsNotFound() throws Exception {
// given
Long nonExistingId = 999L;
when(eventService.getEventDetail(nonExistingId))
.thenThrow(new EventNotFoundException());
// when & then
mockMvc.perform(get("/events/{eventId}", nonExistingId)
...
}
Session Attrs = {SPRING_SECURITY_CONTEXT=SecurityContextImpl [Authentication=UsernamePasswordAuthenticationToken [Principal=com.moonbaar.common.oauth.CustomUserDetails@27a0e6ce, ...]}
로그를 보면 세션 정보에 인증된 사용자 정보가 포함된 걸 알 수 있습니다.
결국 문제의 원인은 다음과 같았습니다:
eventService.getEventDetail()
에 대해 예외를 던지도록 모킹했습니다.@WithMockCustomUser
때문에 인증된 상태였으므로, 컨트롤러는 eventService.getEventDetailForUser()
를 호출했습니다.문제 해결을 위해
@WithMockCustomUser
을 적용했습니다.@WebMvcTest(EventController.class)
@Import(SecurityTestConfig.class)
@AutoConfigureMockMvc(addFilters = false)
// @WithMockCustomUser 제거
class EventControllerTest {
...
@Test
@DisplayName("로그인한 사용자는 좋아요/방문 상태가 포함된 행사 상세 정보를 조회할 수 있다")
@WithMockCustomUser // 인증된 사용자 정보 필요 시에만 메서드 레벨에 적용
void getEventDetail_AuthenticatedUser() throws Exception {
// given
when(eventService.getEventDetailForUser(any(User.class), eq(EVENT_ID)))
.thenReturn(createMockResponse(true, false, 1, 100));
// when & then
mockMvc.perform(get("/events/{eventId}", EVENT_ID))
.andExpect(status().isOk())
.andExpect(jsonPath("$.id").value(EVENT_ID.intValue()))
.andExpect(jsonPath("$.title").value("서울시극단 [코믹]"))
.andExpect(jsonPath("$.isVisited").value(true))
.andExpect(jsonPath("$.isLiked").value(false))
.andExpect(jsonPath("$.visitCount").value(1))
.andExpect(jsonPath("$.likeCount").value(100));
}
@Test
@DisplayName("존재하지 않는 행사 ID로 조회 시 404 응답을 반환한다")
void getEventDetail_WithNonExistingId_ReturnsNotFound() throws Exception {
// given
Long nonExistingId = 999L;
when(eventService.getEventDetail(nonExistingId))
.thenThrow(new EventNotFoundException());
// when & then
mockMvc.perform(get("/events/{eventId}", nonExistingId)
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isNotFound())
.andExpect(jsonPath("$.code").value(EventErrorCode.EVENT_NOT_FOUND.getCode()))
.andExpect(jsonPath("$.message").value(EventErrorCode.EVENT_NOT_FOUND.getMessage()));
}
...
}
컨트롤러 로직 이해하기: 모킹할 때는 실제로 어떤 메서드가 호출될지 정확히 파악해야 합니다. 특히 인증 상태에 따라 다른 동작을 하는 경우 더욱 중요합니다.
인증 상태 관리하기: Spring Security 테스트에서는 다양한 어노테이션을 통해 인증 상태를 설정할 수 있습니다:
@WithMockUser
: 기본 Spring Security 사용자 정보로 인증@WithAnonymousUser
: 익명 사용자로 요청@WithMockCustomUser
): 애플리케이션에 맞는 사용자 정보로 인증어노테이션 적용 범위: 클래스 레벨 어노테이션은 모든 테스트 메서드에 적용되지만, 메서드 레벨 어노테이션으로 오버라이드할 수 있습니다.
테스트 실패 시 응답 로깅: 테스트가 실패할 때 응답 내용을 확인하는 것이 도움이 됩니다:
.andDo(MockMvcResultHandlers.print());
분리된 테스트 클래스 고려: 인증 상태가 다른 테스트들은 별도의 테스트 클래스로 분리하는 것도 좋은 방법입니다.
Spring Security와 함께 컨트롤러 테스트를 작성할 때는 인증 상태와 모킹 설정이 일치하는지 항상 확인해야 합니다.