카테고리 없음

[Board] ArgumentResolver 사용에 따른 LoginController 테스트 코드 작성!! (트러블 슈팅)

문상휘파람 2024. 9. 23. 17:21

이번 테스트 코드를 작성할 때, 어려움이 많았습니다. 

특히 필터와 인터셉터 코드를 처음 작성하였고, 이에 따른 어려움이 특히 크게 느껴졌습니다.

이번 포스팅에서는 테스트 코드 짤 때, 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 를 제외시켰습니다.!!


테스트 코드를 작성하며 궁금증이 들어 한 번 글을 작성해 보았습니다 ㅎㅎ. 비록 초보라 저 방법이 효율적인지는 모르겠지만, 피드백 남겨주셨으면 좋겠습니다! :)