본문 바로가기

Proj/Tripot

Trouble Shooting - @Mock vs @Spy

1. 개요

현재 소셜로그인은 관련 제공사의 확장에 대응하기 위해 다음과 같아 전략 패턴을 사용하고 있다.

    private final List<OAuth2MemberStrategy> oAuth2MemberStrategies;


    /**
     * OAuth2 과정을 프론트 단에서 처리
     * @param response
     * @param oAuth2LoginDto
     * @param provider
     * @return
     */
    public CheckActiveMemberDto oauth2Login(HttpServletResponse response, OAuth2LoginDto oAuth2LoginDto, OAuth2Provider provider) {


        OAuth2UserInfo userInfo = generateOAuth2UserInfo(oAuth2LoginDto, provider);

        String username = userInfo.provider() + " " + userInfo.id();

        boolean existMember = memberRepository.existsByUsername(username);

        Member member = createMember(provider, existMember, username, userInfo);

        makeJWTs(member, response);


        return createResponse(response, member);
    }

    ...

    private OAuth2UserInfo generateOAuth2UserInfo(OAuth2LoginDto oAuth2LoginDto, OAuth2Provider provider) {
        //소셜 로그인 전략 설정
        log.info("[{}] 소셜 로그인 전략 설정 및 정보 추출", Thread.currentThread().getStackTrace()[1].getMethodName());
        return oAuth2MemberStrategies.stream()
                .filter(oAuth2MemberStrategy -> oAuth2MemberStrategy.isTarget(provider))
                .findAny()
                .orElseThrow(() -> new CustomException(StatusCode.OAUTH2_LOGIN_FAILURE))
                .getOAuth2UserInfo(oAuth2LoginDto);
    }

 List를 사용하여 스프링부트의 OAuth2MemberStrategy 인터페이스를 구현한 빈을 조회하고, 이에 맞는 전략을 찾아 회원 정보를 조회한다.

 

    @Test
    @DisplayName("카카오 로그인 - 관련 기능들의 정상 동작 및 해당 dto의 성공적 반환, 새 회원")
    void oauth2LoginV2WithNewMember() {

        //given
        MockHttpServletResponse response = new MockHttpServletResponse();
        OAuth2LoginDto oAuth2LoginDto = OAuth2LoginDto.builder()
                .id("1234")
                .nickname("sample_nickname")
                .build();
        OAuth2Provider kakaoProvider = OAuth2Provider.KAKAO;
        OAuth2UserInfo oAuth2UserInfo = OAuth2UserInfo.builder()
                .id("1234")
                .nickname("sample_nickname")
                .provider(OAuth2Provider.KAKAO)
                .build();

        String sampleAccess = "sample_access_token";
        String sampleRefresh = "sample_refresh_token";


        given(jwtUtil.createJwt(any(LoginCreateJwtDto.class), eq("access"))).willReturn(sampleAccess);
        given(jwtUtil.createJwt(any(LoginCreateJwtDto.class), eq("refresh"))).willReturn(sampleRefresh);
        given(memberRepository.existsByUsername(anyString())).willReturn(false);


        //when
        CheckActiveMemberDto result = oAuth2Service.oauth2Login(response, oAuth2LoginDto, kakaoProvider);

        //then

        //토큰이 헤더에 정상적으로 들어가야 함
        assertThat(response.getHeader("Authorization")).isEqualTo("Bearer " + sampleAccess);
        assertThat(response.getHeader("refresh_token")).isEqualTo("Bearer " + sampleRefresh);

        //새 회원이므로 isActivate는 false여야 함
        assertThat(result.nickname()).isEqualTo("sample_nickname");
        assertThat(result.isActivate()).isFalse();
    }
    
    java.lang.NullPointerException: Cannot invoke "java.util.List.stream()" because "this.oAuth2MemberStrategies" is null

 하지만 서비스, Strategy 모두 mock 객체이고, 이로 인해 로그인 전략을 찾는 부분에서 오류가 발생하게 된다. 이를 해결하기 위해 Mock 객체를 등록하고, 이들이 List에 담겨 사용할 수 있어야 한다.

 

2. @Mock vs @Spy

 @Spy는 @Mock과 달리 실제 인스턴스를 생성하여 Mocking을 수행한다. 에러 메시지에서 List가 실제 인스턴스여야 stream 기능을 정상적으로 수행할 수 있으므로 Spy를 사용해야 한다.

 

    @Spy
    private List<OAuth2MemberStrategy> strategyList = new ArrayList<>();
    @Mock
    private KakaoOAuth2LoginStrategy kakaoOAuth2LoginStrategy;
    @Mock
    private AppleOAuth2LoginStrategy appleOAuth2LoginStrategy;

    @BeforeEach
    public void init(){
        strategyList.add(kakaoOAuth2LoginStrategy);
        strategyList.add(appleOAuth2LoginStrategy);
    }

 실제 리스트를 만들고, 두 전략의 Mock 객체를 만들어 넣어준다.

        OAuth2UserInfo oAuth2UserInfo = OAuth2UserInfo.builder()
                .id("1234")
                .nickname("sample_nickname")
                .provider(OAuth2Provider.KAKAO)
                .build();

        given(kakaoOAuth2LoginStrategy.isTarget(OAuth2Provider.KAKAO)).willReturn(true);
        given(kakaoOAuth2LoginStrategy.getOAuth2UserInfo(oAuth2LoginDto)).willReturn(oAuth2UserInfo);

서비스 계층의 단위 테스트이므로 전략에 대해서는 결과를 정의해준다.

 

소셜 로그인 관련 모든 테스트가 통과하는 것을 확인할 수 있었다.

 

 

3. 참고

https://developer-youngjun.tistory.com/6

 

Mockito : Mock 리스트를 주입하고 테스트 하기

상황 스프링을 사용하여 빈을 주입 받을때, 같은 타입(interface)을 구현한 빈들을 아래와 같이 컬렉션으로 주입 받아 사용하는 경우가 있다. public interface Validator { void validate(Order order); } @Service publ

developer-youngjun.tistory.com

https://coco-log.tistory.com/194

 

Spy와 Mock의 차이. 그리고 Spy 사용

Mock과 Spy는 테스트 더블(대역)이다. Test Double은 테스트를 목적으로 프로덕션 오브젝트를 대체하는 오브젝트를 뜻한다. ‘Test Double’이라는 말 때문에 처음에는 잘 이해가 되지 않았는데 영어권

coco-log.tistory.com