안녕하세요. 회사와 함께 성장하고 싶은 KOSE입니다.

오늘은 의존성 주입을 활용하여 테스트하기 어려운 코드를 테스트해보는 과정을 포스팅하고자 합니다.

 

SpringBoot를 활용하여 단위테스트를 작성할 때, 여러 의존성 주입으로 인해 원하는 부분의 단위 테스트를 작성하지 못할 수 있습니다.

 

예를 들어, A라는 메서드에 B, C의 메소드의 값을 리턴 받거나, 실행되어야 하는 메서드인 경우,  A라는 메서드를 실행하기 위해 B, C 모두 실행되어야 합니다. 아래와 같은 경우, methodA를 단위 테스트하는 과정에 methodB, methodC를 참조하고 있으므로 methodB 및 methodC를 검증한 상태라면 중복되는 테스트라고 볼 수 있습니다.

해당 예시는 처리하기 용이한 과제이기 때문에 테스트 코드 작성하는 것이 난해하지 않지만, 복잡한 과정일 경우, 여간 쉬운 일이 아닙니다.

 

private class A {

    String methodA(String name) {
        B b = new B();
        String bName = b.methodB(name);
        C c = new C();
        String cName = c.methodC(bName);

        if (cName == name) {
            return name;
        }

        return null;
    }

}

private class B {

    String methodB(String name) {
        return name;
    }
}

private class C {
    String methodC(String name) {
        return name;
    }
}

 

1. 문제 상황

이번에 테스트하고자 한 예시는, OAuth2 로그인에서 ProviderUser OAuth2UserService<oAuth2UserRequest, OAuth2User> f를 구현한 CustomOAuth2UserService의 loadUser입니다.

 

먼저, 코드를 제시하겠습니다.

 

@Service
@Slf4j
@Getter
public abstract class AbstractOAuth2UserService {

    private final MemberService memberService;

    public AbstractOAuth2UserService(MemberService memberService) {
        this.memberService = memberService;
    }

    public void register(ProviderUser providerUser, OAuth2UserRequest userRequest){

        Member member = memberService.findByEmailNoOptional(providerUser.getEmail());

        if(member == null){
            ClientRegistration clientRegistration = userRequest.getClientRegistration();
            memberService.register(clientRegistration.getRegistrationId(), providerUser);
        }
    }

    public ProviderUser providerUser(ClientRegistration clientRegistration, OAuth2User oAuth2User) {

        String registrationId = clientRegistration.getRegistrationId();


        switch (registrationId) {

            case "google":
                return new GoogleUser(OAuth2Utils.getMainAttributes(oAuth2User), oAuth2User, clientRegistration);

            case "naver":
                return new NaverUser(OAuth2Utils.getSubAttributes(oAuth2User, "response"), oAuth2User, clientRegistration);

            case "kakao":
                if (oAuth2User instanceof OidcUser) {
                    return new KakaoOidcUser(OAuth2Utils.getMainAttributes(oAuth2User), oAuth2User, clientRegistration);

                } else {
                    return new KakaoUser(OAuth2Utils.getOtherAttributes(oAuth2User, "kakao_account", "profile"), oAuth2User, clientRegistration);
                }

            default:
                return null;
        }
    }
}

  

  • AbstractOAuth2UserService
    • client의 registrationId(socialId)에 따라, 소셜 로그인 방식을 다르게 하는 service 로직입니다.
    • 유저 oauth2 로그인 요청이 발생하면, providerUser 메서드로 ProviderUser 객체 생성 역할을 수행합니다.

 

@Slf4j
@Service
public class CustomOAuth2UserService extends AbstractOAuth2UserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {

    public CustomOAuth2UserService(MemberService memberService) {
        super(memberService);
    }

    @Override
    public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {

        ClientRegistration clientRegistration = userRequest.getClientRegistration();
        OAuth2UserService<OAuth2UserRequest, OAuth2User> oAuth2UserService = new DefaultOAuth2UserService();
        OAuth2User oAuth2User = oAuth2UserService.loadUser(userRequest);

        ProviderUser providerUser = super.providerUser(clientRegistration, oAuth2User);

        super.register(providerUser, userRequest);

        return new PrincipalUser(providerUser);
    }
}
  • CustomOAuth2UserService
    • client의 oauth2 로그인 요청이 처리되는 구현체 클래스로 추상 클래스 AbstractOAuth2UserService를 상속받고, OAuth2UserService<OAuth2UserRequest, OAuth2User> 클래스를 구현합니다.
    • 구현 메서드는 loadUser이고, 해당 리턴 타입은 OAuth2User입니다.
    • PrincipalUser는 OAuth2User 및 폼 로그인 OidcUser를 구현한 공통 로그인 클래스이고, 이번 포스팅에서 검증하고자 하는 대상입니다.

 

 

2. 의존성 주입 파악하기

loadUser 메서드를 테스트하기 위한 방법은 여러 가지가 있습니다. 처음부터 mock 객체를 활용하는 방법도 있지만, 이번에 포스팅하는 주제인 의존성 주입을 활용하여 테스트 코드를 작성한 방법을 공유하겠습니다.

 

의존성 주입이란?

외부에서 두 객체 간의 관계를 결정해주는 디자인 패턴으로, 인터페이스를 사이에 둬서 클래스 레벨에서는 의존관계가 고정되지 않도록 하고 런타임 시에 관계를 동적으로 주입하여 유연성을 확보하고 결합도를 낮출 수 있게 합니다.

 

여기에 왜 필요할까?

해당 소스는 크게 다른 4개의 메서드 혹은 로직이 순서대로 연결되어 있는 메서드입니다. 따라서, loadUser를 테스트하기 위해서는 복잡한 4개의 절차를 거쳐야 합니다.

 

하지만, 단위테스트를 진행할 때, providerUser, registerUser는 이 메서드 이전에도 테스트된 코드일 수 있고, 해당 메서드가 명확하게 OAuth2User를 리턴하는지를 파악하기 위한 코드만 필요하다면, mock객체를 활용하여 테스트 코드를 작성하는 것이 어렵고 복잡한 일이 될 수 있습니다. 이때 활용할 수 있는 방법이 의존성 주입을 활용한 방법입니다.

 

 

CustomOAuth2UserService를 대신할 CustomOAuth2UserServiceMock 클래스와
ProviderUser를 제공해 줄 ProviderUserService 인터페이스를 생성합니다.
DefaultProviderUserService는 ProviderUserService 구현체로
loadUser에 복잡한 로직인 ProviderUser를 생성하는 역할을 수행합니다.
따라서, CustomOAuth2UserServiceMock은 ProviderUser를 받아서
PrincipalUser 객체를 생성하는 로직을 수행하게 됩니다.

 

 

3. 코드로 확인하기

 

  • ProviderUserService
    • ProviderUser 객체를 제공하는 인터페이스 생성하기
interface ProviderUserService {
    ProviderUser getProviderUser(OAuth2UserRequest userRequest);
}

 

  • DefaultProviderUserService 생성
    • ProviderUserService를 구현
    • AbstractOAuth2UserService에 있는 register() 메서드까지 활용하기 위해 추상 클래스 상속
class DefaultProviderUserService extends AbstractOAuth2UserService implements ProviderUserService {

    public DefaultProviderUserService(MemberService memberService) {
        super(memberService);
    }

    @Override
    public ProviderUser getProviderUser(OAuth2UserRequest userRequest) {
        ClientRegistration clientRegistration = userRequest.getClientRegistration();
        OAuth2UserService<OAuth2UserRequest, OAuth2User> oAuth2UserService = new DefaultOAuth2UserService();
        OAuth2User oAuth2User = oAuth2UserService.loadUser(userRequest);

        return providerUser(clientRegistration, oAuth2User);
    }

}

 

  • CustomOAuth2UserServiceMock
    • CustomOAuth2UserService를 대신할 TestClass 생성
public class CustomOAuth2UserServiceMock extends AbstractOAuth2UserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {

    private final ProviderUserService providerUserService;

    public CustomOAuth2UserServiceMock(MemberService memberService,
                                       ProviderUserService providerUserService) {
        super(memberService);
        this.providerUserService = providerUserService;
    }

    @Override
    public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {

        ProviderUser providerUser = providerUserService.getProviderUser(userRequest);

        return new PrincipalUser(providerUser);
    }
}

 

  • Mock을 활용한 테스트 케이스 작성
    • CustomOAuth2UserServiceMock을 활용
    • 중간 과정을 생략하고 PrincipalUser 객체가 생성되는 과정만 테스트를 실행하여 확인할 수 있습니다.
@ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.LENIENT)
class CustomOAuth2UserServiceTest {

    @Mock
    ClientRegistration clientRegistration;

    @Mock
    OAuth2User oAuth2User;

    @Mock
    MemberService memberService;

    @Mock
    ProviderUserService providerUserService;

    @Test
    @DisplayName("oauth2 provider를 체크하기")
    public void providerUser() throws Exception {

        //given
        when(clientRegistration.getRegistrationId()).thenReturn("google");

        //when
        CustomOAuth2UserService oAuth2UserService = new CustomOAuth2UserService(memberService);
        ProviderUser providerUser = oAuth2UserService.providerUser(clientRegistration, oAuth2User);

        //then
        assertThat(providerUser).isInstanceOf(GoogleUser.class);
    }

    @Test
    @DisplayName("oauth2 회원가입 등록하기")
    public void register() throws Exception {
        //given
        OAuth2UserRequest userRequest = mock(OAuth2UserRequest.class);
        when(clientRegistration.getRegistrationId()).thenReturn("google");

        CustomOAuth2UserService oAuth2UserService = mock(CustomOAuth2UserService.class);
        ProviderUser providerUser = oAuth2UserService.providerUser(clientRegistration, oAuth2User);

        //when
        when(providerUserService.getProviderUser(userRequest)).thenReturn(providerUser);
        CustomOAuth2UserServiceMock customOAuth2UserServiceMock = new CustomOAuth2UserServiceMock(memberService, providerUserService);
        OAuth2User oAuth2User = customOAuth2UserServiceMock.loadUser(userRequest);

        //then
        assertThat(oAuth2User).isNotNull();
    }


}

 

 

이상으로 의존성 주입을 활용한 테스트하기 어려운 코드를 테스트하는 과정을 살펴보았습니다. 더 좋은 방법이 있고, 

간단하게 해결할 수 있는 방법이 있다면 조언 부탁드리겠습니다,!

열심히 배우겠습니다.!

 

읽어주셔서 감사드립니다.! 새해 복 많이 받으세요~!

+ Recent posts