ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • TIL.86 Mocking, Patching
    TIL 2021. 1. 2. 23:37
    728x90

    왜? Mocking을 사용해야 할까

    프로젝트 진행간 KaKao 소셜 로그인 API를 맡아 진행하던 중

    프론트에서 카카오플랫폼으로 요청하여 얻은 kakao_token을 전달받고

    전달받은 kakao_token을 이용해 카카오플랫폼으로 전달받은 kakao_token을 이용하여 토큰에 해당하는 유저 정보를 가져와보았다.

    2차 프로젝트 시작 전부터 소셜 로그인 API를 구현해보고자 목표를 세웠다.

    한가지 아쉬운점은 현재 진행하는 Clone 프로젝트는 항공 예약을 주된 기능으로 사용하여 kakao_token을 이용해

    카카오 서버에서 받은 유저 정보를 모두 활용하여 바로 우리 서비스에 가입시킬수 없었다.

     

    현재 프로젝트 중인 서비스에 가입하기 위해선 카카오에서 제공하지 않는 english_name, phone_number 등 필수요구 사항에 해당되는 값들이 없어 바로 회원가입으로 연결하는 로직을 실행해보지는 못하였다.

     

    하지만 kakao_token을 이용해 카카오 서버로 부터 어떤 정보를 가져올 수 있는지 확인 할 수 있었다는것만으로 만족한다

    실제로 프론트/kakao/백엔드 통신으로 우리 서비스에 가입한 사용자가 카카오 소셜 로그인 API를 통해서 로그인할 경우 토큰을 함께 전달해주는 과정도 모두 경험해볼 수 있었다.


    자 그럼 이제 내가 작성한 로직을 바탕으로 테스트 코드를 작성해보려 했다.

    class KakaoSignInView(View):
        def post(self, request):
            access_token = request.headers.get('Authorization', None)
            response     = requests.get('https://kapi.kakao.com/v2/user/me', headers={'Authorization' : f'Bearer {access_token}'}).json()
            user_email   = response['kakao_account']['email']
            user_name    = response['kakao_account']['profile']['nickname']
    
            if User.objects.filter(email=user_email).exists():
                user    = User.objects.get(email=user_email)
                payload = {'user-id' : user.id, 'exp' : datetime.now() +timedelta(hours=1)}
                token   = jwt.encode(payload, SECRET_KEY, algorithm=JWT_ALGORITHM)
                return JsonResponse({'access_token' : token}, status=200)
            return JsonResponse({
                'MESSAGE' : 'NOT_EXIST_USER',
                'EMAiL'   : user_email,
                'NAME'    : user_name,
                },status=403)

    허나 이전에 배웠던 지식만으로는 테스트 코드를 작성하기 너무 어려웠다.

    단위 테스트를 작성하다보면 DB 또는 외부 API에 의존하는 코드를 테스트해야하는 경우가 비일비재하기 마련이다.

    실제로 카카오 소셜 로그인 API를 검증할 테스트 코드는 외부 카카오 소셜 로그인 API에 의존하는 코드이므로 실제 외부 API를 호출하기도 어려울 뿐더러 실행속도 느리고 해당 서비스에 문제가 있을 경우 테스트 코드가 정상적으로 작동하지 못한다고 한다.

     

    이에 대한 새로운 개념으로 Mocking, Patching 을 통해 이를 해결 할 수 있었다.

    (왜 이 2가지를 사용하려고 했는지 이유에 대해서 꼭 짚고 넘어가자)

     

    @patch('user.views.requests')
    # user 앱의 views.py에서 사용될 request를 patch한다는 뜻
        def test_user_post_kakao_signin_success(self, mocked_request):
        #실제 kakao API를 호출하지 않고 kakao API 응답을 Fake로 작성한다
            class FakeResponse:
                def json(self):
                # 아래 형식에서 json 이 필요하다.
                    return {'kakao_account': {
                                'profile': {'nickname': '문타리'},
                                'email': 'applee24@gmail.com',
                                }
                            }
                            # 실제 kakao API로 읽었던 모든 값을 넣으려 하였으나
                            # 로직에서 필요한 nickname 과 email만 사용
    
            mocked_request.get = MagicMock(return_value = FakeResponse())
            # test 할때, requests가 get 메서드로 받은 response는 FakeResponse의 인스턴스이다.
            client = Client()
            header = {'HTTP_Authorization' : 'access_token'}
            # Client의 get method에 header 담기!
            # **extra는 keyword arguments이나 header는 dict이므로 **을 붙여준다.
            # **header를 붙혀주지 않으면 테스트시 에러가 발생함
            response = client.post('/user/signin/kakao', content_type='application/json', **header)
            self.assertEqual(response.status_code, 200)
    @patch('user.views.requests')
        def test_user_post_kakao_signin_not_exist_user(self, mocked_request):
            class FakeResponse:
                def json(self):
                    return {'kakao_account': {
                                'profile': {'nickname': '문타리'},
                                'email': 'AAAAAAA@gmail.com',
                                }
                            }
    
            mocked_request.get = MagicMock(return_value = FakeResponse())
            client = Client()
            header = {'HTTP_Authorization' : 'access_token'}
            response = client.post('/user/signin/kakao', content_type='application/json', **header)
            self.assertEqual(response.status_code, 403)
            self.assertEqual(response.json(),
                {
                    'MESSAGE' : 'NOT_EXIST_USER',
                    'EMAIL' : 'AAAAAAA@gmail.com',
                    'NAME' : '문타리'
                }
            )

    처음시도했던 코드로는 테스트 진행시

    ['kakao_account'], ['email'], ['profile'] KeyError가 발생한다.

      def test_user_post_kakao_signin_success(self):
            self.access_token = 'E5GTwgFwkA4nXAX_5v-gYSDYSmYJ_JkjoX4KwAo9dRsAAAF2wxkBeg'
            self.response     = {'kakao_account': {'profile': {'nickname': '문승희'},'email': 'munshee@naver.com'}}
            self.user_email   = self.response['kakao_account']['email']
            self.user_name    = self.response['kakao_account']['profile']['nickname']
            kakao             = {
                    'access_token' : self.access_token,
                    'response'     : self.response,
                    'user_email'   : self.response['kakao_account']['email'],
                    'user_name'    : self.response['kakao_account']['profile']['nickname']
                    }
            response = client.post('/user/signin/kakao', json.dumps(kakao), content_type='application/json')
            self.assertEqual(response.status_code, 200)
            self.assertEqual(response.json(),
                {
                    'access_token' : response.json()['token']
                }
            )

    자세한 내용은 출처의 블로그를 통해 확인하는것이 훨씬 이해가 빠를것이다


    Mocking이란?

    모의 객체(Mock Object)란 주로 객체 지향 프로그래밍으로 개발한 프로그램을 테스트 할 경우 테스트를 수행할 모듈과 연결되는 외부의 다른 서비스나 모듈들을 실제 사용하는 모듈을 사용하지 않고 실제의 모듈을 "흉내"내는 "가짜" 모듈을 작성하여 테스트의 효용성을 높이는데 사용하는 객체이다. 사용자 인터페이스(UI)나 데이터베이스 테스트 등과 같이 자동화된 테스트를 수행하기 어려운 때 널리 사용된다.

    (출처 : ko.wikipedia.org/wiki/%EB%AA%A8%EC%9D%98_%EA%B0%9D%EC%B2%B4)

    단위 테스트를 작성하다보면 데이터베이스 또는 외부 API에 의존하는 코드를 테스트해야 할 일이 필연적으로 생기기 마련입니다. 운영 환경 대비 제약이 많은 테스트 환경에서는 실제 데이터베이스와 연동하거나 실제 외부 API를 호출하기가 불가능한 경우가 많습니다. 가령 가능하더라도, 이렇게 외부 서비스에 의존하는 테스트는 해당 서비스에 문제가 있을 경우 깨질 수 있으며 실행 속도도 느릴 수 밖에 없습니다.

    따라서 단위 테스트를 작성할 때 외부에 의존하는 부분을 임의의 가짜로 대체하는 기법이 자주 사용되는데 이를 모킹(mocking)이라고 합니다. 다시 말해, 모킹(mocking)은 외부 서비스에 의존하지 않고 독립적으로 실행이 가능한 단위 테스트를 작성하기 위해서 사용되는 테스팅 기법입니다.

    Mock 객체 설정하기

    mocking은 소외 mock이라고 불리는 가짜 객체를 생성하는 것부터 시작합니다. 우리는 이 mock 객체가 어떻게 작동을 할지를 지정해줄 수 있으며, 이 mock 객체는 자신을 상대로 어떤 작업이 일어났는지를 기억합니다.

     

    return_value

    먼저 호출되었을 때 특정 값을 리턴하는 mock 객체는 return_value 옵션을 이용해서 생성

     

    side_effect

    반면에 호출되었을 때 예외가 발생하는 mock 객체는 side_effect 옵션을 이용해서 생성

     

    MagicMock

    파이썬에는 매직 메서드(magic method)라는 개념이 있는데, 모든 객체에는 언어 레벨에서 특수한 목적으로 쓰이는 메서드들을 정의할 수 있습니다. 대표적으로 __str__의 경우, 객체를 읽기 좋은 형태의 문자열로 출력하기 위해서 사용되는 매직 메서드입니다.


    patching? mocking?

    unittest.mock 모듈의 patch() 데코레이터를 이용하면 특정 모듈의 함수나 클래스를 가짜(mock) 객체, 좀 더 엄밀히 말하면, MagicMock 인스턴스로 대체할 수 있습니다. 이 과정을 흔히 mocking또는 patching이라고 하는데, 단위 테스트를 작성할 때 외부 서비스에 의존하지 않고 독립적으로 실행이 가능한 단위 테스트를 작성하기 위해서 사용되는 테스팅 기법입니다.

     

    patch 데코레이터

    unittest.mock 모듈의 patch() 데코레이터는 특정 범위 내에서만 mocking이 가능하도록 해주는데요. 일반적으로 다음과 같이 patching이 필요한 단위 테스트 메서드에 patch() 데코레이터를 선언해줌으로써 해당 메서드 내에서만 patching이 이뤄지게 합니다.

     

    출처 : (좋은자료 감사합니다)

    velog.io/@devzunky/TIL-no.86-Python-Django-Kakao-Social-Login-Unit-Test

     

    TIL no.86 - Django - Kakao Social Login Test

    TIL no.78 - Django - Kakao Social Login (Back End)에서 구현한 Kakao Social Login View의 Test에 대해 포스팅하겠습니다. 1. patch decorator route Mocking하고자 하는 대상이 사용되는 곳을 경로로 잡아야 합니다. 이번 포

    velog.io

    www.daleseo.com/python-unittest-mock-patch/

     

    [파이썬] 테스트 모킹 - patch

    Engineering Blog by Dale Seo

    www.daleseo.com

    www.daleseo.com/python-unittest-mock/

     

    [파이썬] 테스트 모킹 - unittest.mock

    Engineering Blog by Dale Seo

    www.daleseo.com

     

    728x90

    'TIL' 카테고리의 다른 글

    TIL. 95 SQL DDL, DML, DQL  (0) 2021.01.11
    TIL.91 Javascript_Filter 함수  (0) 2021.01.07
    TIL.85 Unit_test_실습_2  (0) 2021.01.01
    TIL.84 Unit Test_실습  (0) 2020.12.31
    TIL.83 Unit Test  (0) 2020.12.30
Designed by Tistory.