들어가며
이전에 Oauth 로그인을 전략 패턴을 활용해서 로직을 분리했다. (https://brightstarit.tistory.com/58)
결과적으로 공통 로직과 provider별 다른 로직을 분리해 낼 수 있었고, 추가적으로 내가 제어할 수 없는 로직을 분리했다.
이제 테스트를 진행하려는데 맞닥들인 문제가 있었다.
- Mockmvc 테스트를 진행하면서 내가 제어할 수 없는 부분은 어떻게 처리해야 할까?
- Map으로 동적으로 사용할 빈을 결정하는 경우 Stub을 어떻게 해야 할까?
이 두 문제를 해결함으로써 좀 더 테스트를 진행할 때 문제에 더 유연하게 대처가 가능해질 것 같다.
내가 제어할 수 없는 코드 테스트
다음은 우리 서비스의 /api/login
을 호출했을 때의 코드이다.
@Override
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response)
throws AuthenticationException {
try {
LoginReqDto loginReqDto = om.readValue(request.getInputStream(), LoginReqDto.class); //1
//provider validation
if (Arrays.stream(PROVIDERS)
.noneMatch((provider) -> provider.equals(loginReqDto.getProvider().toUpperCase()))) {
throw new RuntimeException("유효하지않은 provider 입니다.");
} //2
// oauth login
User user = oauthLoginService.attemptLogin(loginReqDto); //제어할 수 없는 코드 //3
// 강제 로그인
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(user.getEmail(), "DEFAULT_PASSWORD");
// UserDetailsService의 loadUserByUsername 호출
// JWT를 쓴다 하더라도, 컨트롤러 진입을 하면 시큐리티의 권한체크, 인증체크의 도움을 받을 수 있게 세션을 만든다.
return authenticationManager.authenticate(authenticationToken); //4
} catch (Exception e) {
throw new InternalAuthenticationServiceException(e.getMessage());
}
}
과정은 다음과 같다.
HttpServletRequest로부터
loginReqDto
를 받는다.- 해당 DTO에서 provider의 정보를 받고 우리 서비스가 제공하는 provider인지 유효성 검사를 한다.
- DTO에 있는 토큰을 통해 토큰을 검증하고 검증된 사용자를 가져온다. (제어할 수 없는 코드)
- 사용자를 강제 로그인시킨다.
이 중 3번 과정이 문제다.
위의 로직이 정상적으로 작동하면 successfulAuthentication
함수가 실행되어 Response에 Httponly Cookie로 refresh Token과 Header로 access Token, 그리고 사용자 정보가 담기게 된다.
Mockmvc 테스트에서는 이 로그인이 성공했을 때 나온 Response를 검증하고 싶다.
하지만 Request로 받는 토큰은 내가 임의로 생성할 수 없기에 oauthLoginService.attemptLogin(loginReqDto)
해당 코드는 무조건 실패하게 된다.
Mockito를 활용하여 Mock 객체를 임의로 넣을 수도 없고 막막했었다.
한참을 고민하다가 진행하던 스터디원분의 도움을 얻어 @Profile
을 활용하는 방안을 생각했다.
3번 과정의 OauthLoginService를 인터페이스로 구성하고 Test용 구현체를 따로 만들어 User객체를 적용하도록 했다.
Test용 구현체
@Service
@Profile("test")
public class TestOauthLoginService implements OauthLoginService {
@Override
public User attemptLogin(LoginReqDto loginReqDto) {
return User
.builder()
.email("ssar@naver.com")
.password("DEFAULT_PASSWORD")
.build();
}
}
실제 구현체
import java.util.Map;
@Service
@Slf4j
@RequiredArgsConstructor
@Profile("!test")
@Transactional
public class OauthLoginServiceImpl implements OauthLoginService {
//....
}
실제 구현체는 로직이 길어 생략했다.
여기서 @Profile
을 적용해 test에는 위의 로직을, test가 아닌 경우에는 아래 로직을 적용하도록 했다.
이제 테스트를 실행해 보자.
결과적으로 cookie와 header가 정상적으로 response 되는 것을 확인할 수 있었다.
Map 데이터 타입으로 Mock객체 주입
다음은 제어하지 못하는 함수인 oauthLoginService.attemptLogin(loginReqDto)
로직을 Mockito를 통해 테스트해 보자.
@Service
@Slf4j
@RequiredArgsConstructor
@Profile("!test")
@Transactional
public class OauthLoginServiceImpl implements OauthLoginService {
//동적으로 빈 사용
private final Map<String, OauthService> oauthServiceMap;
private final UserRepository userRepository;
private final BCryptPasswordEncoder bCryptPasswordEncoder;
public User attemptLogin(LoginReqDto loginReqDto) {
OauthService oauthService = oauthServiceMap.get(loginReqDto.getProvider().toUpperCase()); //1
Map<String, String> payload = oauthService.getPayload(loginReqDto.getToken()); //2
String email = payload.get("email");
String name = payload.get("name");
try {
// 이미 가입된 사용자가 있을 경우에
return getJoinedUser(email); //3
} catch (Exception e) {
// 가입된 사용자가 없을 경우에 가입한다.
log.debug("이메일에 해당하는 사용자가 없습니다.");
return joinUser(loginReqDto, email, name); //4
}
}
private User joinUser(LoginReqDto loginReqDto, String email, String name) {
//...
return user;
}
private User getJoinedUser(String email) {
//...
}
해당 로직은 다음과 같다.
LoginReqDto
를 통해 provider를 가져와OauthService
의 구현체를 결정한다.LoginReqDto
를 통해 token을 가져와 토큰 검증을 하고 payload를 가져온다.- 가져온 payload를 통해 이미 가입된 사용자라면 사용자 정보를 가져온다.
- 가져온 payload를 통해 가입이 안된 사용자라면 새로 가입시킨다.
여기서 부딪혔던 문제는 1번 과정에서 OauthServiceMap
을 Mock 객체로 주입하고, OauthServiceMap.get()
으로 return 된 OauthService
도 Mock객체로 만들어줘야 한다는 것이다.
해결 과정은 다음과 같다.
OauthServiceMap
을 Mock객체로 주입을 한다.mock()
함수를 통해OauthService
의 구현체의 Mock객체를 생성한다.OauthServiceMap
의 return 값을 2번 과정에서 생성된 Mock객체를 return 하도록 한다.- 2번 과정에서 생성된
OauthService
의getPayload()
는 임의의 Map데이터를 return 하도록 한다.
위와 같은 과정의 테스트는 다음과 같이 작성했다.
결과적으로 사용자가 가입이 된 상태와 안된 상태일 때 각각 정상적으로 동작하는지 테스트해 볼 수 있었다.
그리고 mock()
을 어떤 경우에 사용하면 좋을지 감이 잡힌 과정이었다.
'프로젝트 > Petogram' 카테고리의 다른 글
[Petogram] Oauth Login refactoring 여행기 - 1 (0) | 2023.06.16 |
---|