Project2 - Unit Test

Heechul Yoon·2020년 3월 14일
4

LOG

목록 보기
31/62

작성한 코드의 작동여부를 검사하기 위해 테스트를 시행한다. 여러가지의 테스트방법 작성단계에서는 시간이 걸리지만 한번 작성해놓으면 거의 모든 기능을 자동으로 테스트 할 수 있어 유지보수에 유리한 유닛테스트를 알아보자.

우선 테스트에는 어떤종류가 있을까?

테스트 유형

End-to-End(UI) test

실재로 배포된 사이트가 제대로 렌더링이 되는지 브라우저 상에서 확인을 하는 것이다. 시스템을 전부 갖추고 사람이 직접 실행하기 때문에 직관적이고, 테스트를 커스터마이징 할 수 있다는 장점이 있지만 테스트 사항을 누락하는 경우도 생긴다.

Integration test

실재 서비스를 테스트 하는 것이 아닌 프론트엔드 서버와 백엔드 서버만 서로 연결해서 mock data를 주고받는 것을 말한다. end to end testing보단 아니지만 여전히 모든 예외상황을 자동화 하는 것이 아닌 사람이 테스트를 하기 때문에 테스트 사항이 누락될 수 있다.

Unit test

함수의 리턴을 기준으로 테스트를 나누어 코드로 작성한다. 사람이 직접 모든 케이스를 테스트 하는것이 아닌 코드를 통해서 자동으로 테스트하기 때문에 테스트가 누락 될 확률 이 거의없으며 빠르게 테스트를 할 수 있다. 그리고 버전이 업데이트 될 때 마다 시행해야 하는 테스트를 자동화 할 수 있어 유지보수에 효과적이다.

Test의 일반적인 원칙

  • 테스트는 각 기능의 가장 세부적인 리턴을 단위로 하여 동작 되는지 검증해야한다.
  • 각 테스트의 유닛은 서로다른 테스트 유닛에 영향을 미쳐서는 안되고 독립적이어야 한다.
  • 테스트가 빠르고 효과적이게 진행되기 위해 노력해야한다.
  • 코딩으 이전작업에 이어서 시작하기 전에 항상 풀 테스트 슈트를 돌려 이전의 커밋 작동여부를 확인한다.

Unit Test Intro

그렇다면 이제 2차프로젝트(사운드 클라우드 클론코딩)에서 작성한 회원가입, 로그인, 데코레이터, 메세지, 팔로우 기능의 유닛테스트를 간략하게 해보자.

유닛테스트는 가상의 데이터베이스와 가상의 request를 통해서 코드의 작동여부를 확인한다.

  • 가상의 데이터베이스에 한 기능의 테스트에 필요한 데이터를 생성하고, 테스트가 끝나면 지워주는 코드를 작성한다.(다른 기능 테스트의 테스트환경을 독립적으로 하기 위해서)
  • 기능이 정상적으로 작동했을 때의경우, 애러를 발생시키는 경우, 예외의 경우 등 독립적인 리턴값과 except값을 하나의 유닛으로 나누어 코드륵 작성한다.

하나의 기능당 테스트할 유닛이 너무 많은 관계로 기능별 대표적인 몇개의 테스트만 살펴보자.

회원가입 Unit Test

setup

from django.test                import TestCase, Client [0]

class SignUpTest(TestCase):                  [1]
    def setUp(self):                         [2]
        User.objects.create(
            email         = 'aaa@aaa.com',
            password      = 'aaaaaaaa',
            age           = 28,
            name          = 'heechul',
            gender        = 'Male',
            profile_image = 'aaaaaaaa',
        )

    def tearDown(self):                      [3]
        User.objects.all().delete()

[0] 필요한 모듈을 import한다. Testcase는 유닛테스트 클래스의 부모클래스로 유닛테스트를 위해서 각각의 기능별 클래스에 넣어 Testcase의 성격을 상속시켜준다. Client는 가상의 매서드를 테스트의 대상인 view로 보내기 위해서 사용한다.
[1] View에서도 하나의 기능은 하나의 View로 구분되어 있기 때문에 해당 기능의 테스트를 묶음을 클래스로 만들어준다.
[2] 이 기능의 테스트를 위해서 필요한 데이터 또는 환경을 seUp매서드를 따로 만들어 생성해준다. 회원가입의 경우 유저 중복여부를 확인하기 위해 임의의 유저정보 데이터를 미리 생성한다.
[3] 기능별 독립성을 위해서 해당 기능의 테스트를 위해 만들었던 데이터는 가상의 데이터베이스에서 지워준다.

sign up success

    def test_sign_up_success(self):                     
[1]     user = {                                              
            'email'         : 'heechul@heechul.com',
            'password'      : 'heechulheechul',
            'age'           : '28',
            'name'          : 'heechul',
            'gender'        : 'male',
            'profile_image' : 'heechul'
        }

[2]     client   = Client()
[3]     response = client.post('/user/sign-up', json.dumps(user), content_type = 'application/json')
[4]     self.assertEqual(response.status_code, 200)

[1] 해당 기능이 성공적으로 동작하기 위해서 유저가 실재로 입력해야 하는 데이터를 json형식으로 바꾸기 위해 딕셔너리 형태의 객체로 만들어준다.
[2] 테스트의 대상 View에 요청을 보내기 위해서 클라이언트를 만들어준다.
[3] 앤드포인트 경로를 설정해주고, [1]에서 유저가 입력해야하는 데이터를 json으로 바꿔주고, 데이터형식을 json으로 헤더의 메타더에터 정보로서 설정해준다.
[4] 테스트 대상 view의 리턴 status와 내가 의도하여 만든 테스트 유닛의 리턴 status를 비교한다.
성공했다면 아래와 같은 결과가 나와 테스트가 통과된다.

200(테스트대상 View의 status결과) == 200(내가 의도한 status결과)

sign up user exists

이제 유저가 이미 존재하는 이메일을 input으로 넣었을 경우의 애러를 유도해서 테스트 코드를 작성해보자.

    def test_sign_up_user_exists(self):
[1]     user = {
            'email'         : 'aaa@aaa.com',
            'password'      : 'aaaaaaaa',
            'name'          : 'heechul',
            'age'           : '28',
            'gender'        : 'male',
            'profile_image' : 'heechul'
        }

        client   = Client()
        response = client.post('/user/sign-up', json.dumps(user), content_type = 'application/json')
[2]     self.assertEqual(response.status_code, 400)
[3]     self.assertEqual(response.json(), {'message' : 'USER_EXISTS'})

[1] 처음에 setUp매서드에서 데이터베이스에 임의로 만들었던 유저데이터와 이메일이 같게 설정한다.
[2] 실재 view에서 리턴하는 status를 적어준다. view에서 리턴하는 상태코드는 400인데 테스트에서는 401이라고 지정하면 테스트 실패 메세지가 뜬다.
[3] 실재 view에서 리튼하는 json메세지와 동일한 메세지를 적어줘야 테스트에 통과한다.

메세지 기능 Unit Test(decorator check)

SNS에서 내가 다른유저에게 메세지를 보낼 때를 상상해보자. 내가 로그인하지 않은 상태에서는 다른 유저에게 메세지를 보낼 수 없다. 즉, 메세지는 로그인 한 회원의 고유 권한이다.

그래서 테스트를 위해서는 메세지 기능을 이용하기 위해서 토큰생성을 먼저한다. 그리고 request에 토큰을 실어보내어 MessageView의 데코레이터를 통과한 이후에 실재 MessageView를 테스트 할 수 있다.

setup

class MessageTest(TestCase):
[1] def setUp(self):
        User.objects.create(email='aaa@aaa.com', password='aaaaaaaa', name='aaa', gender='male', profile_image='aaa')
        User.objects.create(email='bbb@bbb.com', password='bbbbbbbb', name='bbb', gender='male', profile_image='bbb')
        Playlist.objects.create(name='good')
        Song.objects.create(name='hi', artists='v')
[2]     self.token = jwt.encode({'user_id' : User.objects.get(email = 'aaa@aaa.com').id}, SECRET_KEY, algorithm = ALGORITHM)
[3]     self.message = Message.objects.create(
                content = 'hi',
                from_user_id = 1,
                to_user_id = 2,
        )

[1] 메세지 기능은 기존에 메세지 테이블이 참조하는 테이블의 데이터가 있을 때 테스트가 가능하기 때문에 미리 메세지에 포함 될 수 있는 유저(두명이상), 플레이리스트, 노래를 데이터베이스에 create해둔다.
[2] 실재로는 로그인을 해서 토큰을 받지만 각각의 테스트가 독립적이어야 하기 때문에 만들어진 유저데이터(이 경우에는 이메일이 aaa@aaa.com)를 기반으로 토큰을 생성하고 클래스 전역에서 사용하기 위해 클래스 객체로 만들어준다.
[3] 메세지의 get매서드를 테스트하기 위해서 미리 만들어진 유저데이터를 기반으로 메세지테이블의 객체를 하나 만들어 번수로 저장해둔다. 그리고 클래스 전역의 get기능 테스트에서 사용하기 위해 클래스의 객체로 만들어준다.

get overall message from user

주체가되는 유저가 여러유저와 주고받은 메세지 내용을 보여주는 message_all기능을 테스트해보자

    def test_message_get_all_message_success(self):
        client   = Client()
[1]     header   = {"HTTP_Authorization" : self.token}
[2]     response = client.get('/user/message', **header)
        self.assertEqual(response.status_code, 200)

[1] setUp에서 만들어 줬던 토큰값을 헤더에 넣어 변수로 저장한다.
[2] 실재 클라이언트 request에 토큰정보를 넣은 헤더값을 endpoint adress와 함께 넣는다. 이로서 데코레이터의 기능까지 테스트가 가능하다.
실재 json으로 리턴되는 값은 테스트 단계에서 확인할 수 없기 때문에 json리턴값까지 의도한 데로 일치하는지는 확인 할 수 없다.

get compiled message from user

주체가 되는 유저가 한 유저와 주고받은 메세지의 히스토리를 보여주는 message_to_user기능을 테스트해보자

    def test_message_get_to_user_message_success(self):
        client   = Client()
[1]     header   = {"HTTP_Authorization" : self.token}
[2]     response = client.get('/user/message?to_user=2', **header)
        self.assertEqual(response.status_code, 200)

[1] setUp에서 만들어 주었던 토큰값을 헤더에 넣어 변수로 지정한다.
[2] 만들었던 토큰정보가 담긴 header값과 함께 endpoint address를 넣는다. 여기서 주의할 점은 query parameter로 주체가 되는 유저가 메세지를 보내는 대상이 들어간다. 이 경우에는 1번유저(이 유저 정보를 기반으로 토큰이 발행되었음)가 2번유저에게 메세지를 보낸다.

post message to user

이제 주체가되는 유저가 특정유저(to_user)에게 메세지를 보내는 기능을 테스트해보자.

    def test_message_post_success(self):
        client  = Client()
[1]     header  = {"HTTP_Authorization" : self.token}
[2]     message = {
                'from_user_id' : 1,
                'to_user_id'   : 2,
                'content'      : 'how are you',
                'playlist_id'  : 1,
                'song_id'      : 2,
        }
[3]     response = client.post('/user/message', json.dumps(message), **header, content_type = 'application/json')
        self.assertEqual(response.status_code, 200)

[1] 토큰이 있는 유저(로그인 한 유저)만 메세지를 실재로 보낼 수 있기 때문에 토큰을 헤더값에 넣어준다.
[2] 실재 유저가 입력할 데이터를 json형식으로 변환되기 위해 dictionary로 만들어준다. 메세지 테이블과 many to many관계에 있는 playlist와 song도 잘 저장되는지 확인하기 위해 값을 넣어주자
[3] 토큰을 endpoint address와 함께 실어서 보낸다.

팔로우기능 Unit Test(decorator check)

setup

팔로우 기능검증에 사용될 셋업데이터를 설정해주자.

    def setUp(self):
[1]     User.objects.create(email='aaa@aaa.com', password='aaaaaaaa', name='aaa', gender='male', profile_image='aaa')
        User.objects.create(email='bbb@bbb.com', password='bbbbbbbb', name='bbb', gender='male', profile_image='bbb')
[2]     self.token = jwt.encode({'user_id' : User.objects.get(email = 'aaa@aaa.com').id}, SECRET_KEY, algorithm = ALGORITHM)

[1] 팔로우기능도 두명이상의 유저정보가 필요하기 때문에 미리 가상의 데이터베이스에 만들어둔다.
[2] 로그인하지 않은 유저는 팔로우를 할 수 없기 때문에 토큰을 만들어진 유저정보를 바탕으로 임의로 만들어 클래스객체로 지정한다.

post follow data

어떤유저가 어떤유저를 팔로우 했다는 사실을 데이터베이스에 저장하는 기능을 unit test로 검증하는 코드를 작성해보자.

    def test_follow_post_success(self):
        client = Client()
[1]     header = {"HTTP_Authorization" : self.token}
[2]     follow = {
                'followee_id' : 2,
        }
        response = client.post('/user/follow', json.dumps(follow), **header, content_type = 'application/json')
        self.assertEqual(response.status_code, 200)

[1] 로그인여부를 확인하기 위해 클래스 객체로 만들어진 토큰을 헤더값에 넣는다.
[2] followee_id는 팔로우 당하는 사람의 아이디이다. 여기서 팔로우 하는 사람의 아이디는 왜 입력을 안받는지 의문이 생길 수 있다. view의 코드를 살펴보자

[a] @login_required
[b] def post(self, request):
        data = json.loads(request.body)
[c]     user = request.user.id
        if user == data['followee_id']:
            return JsonResponse({'message' : 'SELF_FOLLOWING'}, status = 400)

        Follow.objects.create(
[d]             follower_id = user,
                followee_id = data['followee_id'],
        )

test.py의 [1]에서 헤더에 담긴 토큰값은 views.py에서 [a]의 데코레이터로 넘어가 request에 유저정보가 decode되어 객체로서 들어간다. [b]에서 post매서드가 데코레이터에서 넘겨받은 request에는 유저정보가 객체로 담겨져 있다. [c]에서 request에 담겨져있는 유저객체의 id값을 변수로 지정한다.
[d]에서 유저의 id값을 담고있는 user변수를 follower_id(팔로우를 하는 유저)의 id로 넣어져 Follow테이블에 create된다.

다시 test.py로 돌아와 [2]에서 followee_id만 넣어서 view로 클라이언트의 테스트 포스트가 넘어가면 위의 로직을 거쳐 자동으로 토큰정보를 이용한 follower_id가 생성되어 저장되는 것이다.

이제 테스트를 실행해보자

python manage.py test

모든 유닛테스트가 통과되면 아래와 같은 메세지가 뜨고, 통과되지않으면 error메세지가 뜬다.

Google 로그인 Unit Test

구글로그인 view가 만들어졌다면 이것을 테스트하는 unit test를 만들어보자.

class GoogleSignInTest(TestCase):
    def setUp(self):
        User.objects.create(
                email = 'aaa@aaa.com',
                name  = 'aaaa',
        )

우선 임의로 테스트 데이터베이스에 구글 로그인 유닛테스트 어카운트를 만들어 테스트 환경을 설정해준다.

    def tearDown(self):
        User.objects.all().delete()

셋업으로 만든 값을 다음테스트의 환경에 영향을 미치지 않기 위해서 테스트 데이터베이스에서 지워준다.

from unittest.mock import patch, MagicMock [0]

[1] @patch('user.views.requests')
[2] def test_google_sign_up_success(self, mocked_request):
        client = Client()

[3]     class MockedResponse:
            def json(self):
[4]             return {
                        'email' : 'bbb@bbb.com',
                        'name'  : 'bbbb',
                }

[5]     mocked_request.get = MagicMock(return_value = MockedResponse())
[6]     header = {'HTTP_Authorization' : 'google_auth_token'}
[7]     response = client.post('/user/sign-up/google', content_type = 'applications/json', **header)
        self.assertEqual(response.status_code, 200)

[1] user앱안의 views파일에 requests의 요청대상을 해당 매서드로 지정하는 데코레이터
[2] patch에서 지정해주었던 requests의 요청을 mocked_request로 가져온다.
[3] mocked_request에 대한 response값을 클래스를 만들어서 지정해준다
[4] 실재 구글이 보내주는 payload 키값을 임의로 설정한다.
[5] mocked_request의 값을 MockedResponse의 실행값으로 한다.
[6] header에 임의의 google_auth 토큰을 넣어준다. 실재 구글에 요청을 보내는것이 아니고 지금의 테스트클래스를 구글api로 대체하는 것이기 때문에 아무값이나 넣어줘도 상관없다.
[7] 구글 로그인 api로 header를 포함한 요청을 보낸다.

profile
Quit talking, Begin doing

2개의 댓글

comment-user-thumbnail
2020년 7월 13일

오늘도 감사히 도움이 됐습ㄴㅣ다 heechul yoon

1개의 답글