이번 테스트 코드를 작성할 때, 어려움이 많았습니다.
특히 필터와 인터셉터 코드를 처음 작성하였고, 이에 따른 어려움이 특히 크게 느껴졌습니다.
이번 포스팅에서는 테스트 코드 짤 때, ArgumentResolver를 어떠한 방식으로 처리했는지 작성해 보겠습니다.
조금 더 명확히 명시하자면 ArgumentResolver 로직이 필요없는(사용될 필요가 없는) 컨트롤러 테스트 코드 작성입니다!!
예를 들어, 제가 설명하고자 하는 로그인 로직이 있겠습니다!
이번 미션에서 cookie, session, jwt 에 관한 로그인 로직을 작성하였고, 리졸버와 인터셉터는 jwt에 관해서만 로직을 작성하였습니다!
따라서 CookieLoginController , SessionLoginController 테스트 코드는 리졸버와 인터셉터에 상관 없이 작동되어야 했습니다..!!
이에 관해서, 저는 CookieLoginController를 중심으로 설명드리고자 합니다.
* MemberController
@RestController
@RequestMapping("/api")
@AllArgsConstructor
@Slf4j
public class MemberController {
private final MemberService memberService;
@PostMapping("/members")
public ResponseEntity<Void> createUser(@RequestBody MemberRequest memberRequest) {
MemberResponse memberResponse = MemberMapper.toMemberResponse(memberService.signUp(memberRequest));
URI location = URI.create("/api/members/" + memberResponse.id());
log.info("{}님 회원가입 성공.", memberResponse.memberNickName());
return ResponseEntity.created(location).build();
}
@GetMapping("/members")
public ResponseEntity<MemberResponse> showMember(@Login Long memberId) {
MemberResponse memberResponse = MemberMapper.toMemberResponse(memberService.findMember(memberId));
log.info("{}님 정보 조회하였습니다.", memberResponse.memberNickName());
return ResponseEntity.ok(memberResponse);
}
@PatchMapping("/members")
public ResponseEntity<MemberResponse> updateMember(@RequestBody MemberRequest memberRequest, @Login Long memberId) {
MemberResponse memberResponse = MemberMapper.toMemberResponse(memberService.updateMember(memberRequest, memberId));
log.info("{}님의 회원정보가 수정되었습니다.", memberResponse.memberNickName());
return ResponseEntity.ok(memberResponse);
}
@DeleteMapping("/members")
public ResponseEntity<MemberResponse> deleteMember(@Login Long memberId) {
MemberResponse memberResponse = MemberMapper.toMemberResponse(memberService.deleteMember(memberId));
log.info("{}님 탈퇴하였습니다.", memberResponse.memberNickName());
return ResponseEntity.ok(memberResponse);
}
}
작성한 MemberController 로직입니다. 회원가입 메서드를 제외한 나머지는 마이페이지 로직으로, @Login 어노테이션을 통해 리졸버를 호출하는 것을 알 수 있습니다.
*MemberControllerTest
@WebMvcTest(MemberController.class)
public class MemberControllerTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private ObjectMapper objectMapper;
@MockBean
private MemberService memberService;
private MemberResponse memberResponse;
private MemberRequest memberRequest;
@BeforeEach
void setUp() {
memberResponse = new MemberResponse(1L, "jay", "jj", "aaa", "bbb");
memberRequest = new MemberRequest("jay", "jj", "aaa", "bbb");
}
@DisplayName("회원가입 테스트.")
@Test
void signup() throws Exception {
//given
Member newMember = new Member(1L, "jay", "jj", "aaa", "bbb");
when(memberService.signUp(memberRequest)).thenReturn(newMember);
//when&then
mockMvc.perform(post("/api/members")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(memberRequest)))
.andExpect(status().isCreated())
.andExpect(header().string("Location", "/api/members/1"))
.andDo(print());
}
}
MemberController 테스트 로직입니다. 회원가입 메서드만 작성하였는데, 올바르게 작동하였습니다. @Login 어노테이션이 필요 없는 로직이였기에 리졸버가 따로 활성화 되지 않았습니다.
*CookieLoginController
@RestController
@RequestMapping("/api")
@AllArgsConstructor
@Slf4j
public class CookieLoginController {
private final CookieLoginService cookieLoginService;
@PostMapping("/members/cookie/login")
public ResponseEntity<Void> login(@RequestBody LoginRequest loginRequest, HttpServletResponse response) {
LoginResponse loginResponse = MemberMapper.toLoginResponse(cookieLoginService.login(loginRequest));
Cookie cookieCreate = new Cookie("memberId", String.valueOf(loginResponse.memberId()));
cookieCreate.setMaxAge(60 * 60);
cookieCreate.setPath("/api");
response.addCookie(cookieCreate);
CookieStorage.setCookie(loginResponse.memberId(), cookieCreate);
log.info("{}님 로그인 성공.", loginResponse.memberNickName());
return ResponseEntity.status(HttpStatus.OK).build();
}
@PostMapping("members/cookie/logout")
public ResponseEntity<Void> logout(HttpServletResponse response, HttpServletRequest request) {
LogoutResponse logoutResponse = MemberMapper.toLogoutResponse(cookieLoginService.findMemberByCookie(request));
Cookie cookieRemove = new Cookie("memberId", "");
cookieRemove.setMaxAge(0);
response.addCookie(cookieRemove);
CookieStorage.deleteCookie(logoutResponse.memberId());
log.info("{}님 로그아웃 성공.", logoutResponse.memberNickName());
return ResponseEntity.status(HttpStatus.OK).build();
}
}
쿠키 로그인 로직입니다. @Login 어노테이션을 호출하는 로직이 보이지 않습니다. 그렇다면 리졸버가 활성화 되지 않으니, 그냥 테스트 코드를 작성하여도 아무 문제가 없을까요?? -> 답은 아닙니다.!
@WebMvcTest(controllers = CookieLoginController.class)
public class CookieLoginControllerTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private ObjectMapper objectMapper;
@MockBean
private CookieLoginService cookieLoginService;
@DisplayName("쿠키 로그인 테스트.")
@Test
void cookie_login() throws Exception {
//given
LoginRequest loginRequest = new LoginRequest("aaa", "bbb");
Member loginMember = new Member(1L, "jay", "jj", "aaa", "bbb");
when(cookieLoginService.login(loginRequest)).thenReturn(loginMember);
//when&then
mockMvc.perform(post("/api/members/cookie/login")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(loginRequest)))
.andExpect(status().isOk())
.andDo(print());
}
@DisplayName("쿠키 로그아웃 테스트.")
@Test
void cookie_logout() throws Exception {
//given
Member logoutMember = new Member(1L, "jay", "jj", "aaa", "bbb");
Cookie cookie = new Cookie("memberId", String.valueOf(logoutMember.getId()));
when(cookieLoginService.findMemberByCookie(Mockito.any(HttpServletRequest.class))).thenReturn(logoutMember);
//when&then
mockMvc.perform(post("/api/members/cookie/logout")
.cookie(cookie))
.andExpect(status().isOk())
.andExpect(cookie().value("memberId", ""))
.andDo(print());
}
}
이러한 방식대로 테스트 코드를 작성한다면 다음과 같은 오류를 만나보실 수 있습니다 ㅎㅎ..
그렇다면 쿠키로그인 로직은 리졸버를 호출하지도 않는데 왜 오류가 나게 될까요??
결론
이유는 다음과 같습니다!
1. MemberController에서 문제가 없는 이유.
: MemberControllerTest에서 createUser는 @Login 어노테이션을 사용하지 않기 때문에 LoginArgumentResolver가 호출될 필요가 없습니다. 비록 Spring 컨텍스트가 로드될 때 LoginArgumentResolver가 활성화되지만, 해당 테스트에서 @Login이 없으므로 그 리졸버는 실행되지 않고, 따라서 문제가 발생하지 않습니다.
(그리고 나머지 로직에서 어차피 리졸버를 호출해야 하기 때문에 문제가 생기지 않습니다.)
2. CookieLoginControllerTest에서 예외가 발생하는 이유.
: CookieLoginControllerTest에서 문제가 되는 이유는, WebMvcTest를 사용하면 Spring이 컨트롤러에서 사용하는 모든 리졸버와 인터셉터를 자동으로 로드하기 때문입니다. 따라서 LoginArgumentResolver 로드 되긴 하지만, @Login을 사용하지 않으므로 리졸버가 활성화 되지는 않습니다. 그렇지만, LoginArgumentResolver가 WebMvcTest 환경에서 WebConfig에 등록된 상태에서 자동으로 로드되면, 만약 테스트에 포함된 다른 부분에서 리졸버가 잘못 호출되거나, 컨텍스트 내에서 불필요한 의존성 문제가 발생할 수 있습니다. 그래서 명시적으로 LoginArgumentResolver를 제외해야 합니다.
해결책
@WebMvcTest(controllers = CookieLoginController.class, excludeFilters = @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = {WebConfig.class, LoginArgumentResolver.class}))
저는 요런식으로 리졸버 클래스와 리졸버를 등록한 WebConfig 를 제외시켰습니다.!!
테스트 코드를 작성하며 궁금증이 들어 한 번 글을 작성해 보았습니다 ㅎㅎ. 비록 초보라 저 방법이 효율적인지는 모르겠지만, 피드백 남겨주셨으면 좋겠습니다! :)