작성한 코드의 작동여부를 검사하기 위해 테스트를 시행한다. 여러가지의 테스트방법 작성단계에서는 시간이 걸리지만 한번 작성해놓으면 거의 모든 기능을 자동으로 테스트 할 수 있어 유지보수에 유리한 유닛테스트를 알아보자.
우선 테스트에는 어떤종류가 있을까?
실재로 배포된 사이트가 제대로 렌더링이 되는지 브라우저 상에서 확인을 하는 것이다. 시스템을 전부 갖추고 사람이 직접 실행하기 때문에 직관적이고, 테스트를 커스터마이징 할 수 있다는 장점이 있지만 테스트 사항을 누락하는 경우도 생긴다.
실재 서비스를 테스트 하는 것이 아닌 프론트엔드 서버와 백엔드 서버만 서로 연결해서 mock data를 주고받는 것을 말한다. end to end testing보단 아니지만 여전히 모든 예외상황을 자동화 하는 것이 아닌 사람이 테스트를 하기 때문에 테스트 사항이 누락될 수 있다.
함수의 리턴을 기준으로 테스트를 나누어 코드로 작성한다. 사람이 직접 모든 케이스를 테스트 하는것이 아닌 코드를 통해서 자동으로 테스트하기 때문에 테스트가 누락 될 확률 이 거의없으며 빠르게 테스트를 할 수 있다. 그리고 버전이 업데이트 될 때 마다 시행해야 하는 테스트를 자동화 할 수 있어 유지보수에 효과적이다.
그렇다면 이제 2차프로젝트(사운드 클라우드 클론코딩)에서 작성한 회원가입, 로그인, 데코레이터, 메세지, 팔로우 기능의 유닛테스트를 간략하게 해보자.
유닛테스트는 가상의 데이터베이스와 가상의 request를 통해서 코드의 작동여부를 확인한다.
하나의 기능당 테스트할 유닛이 너무 많은 관계로 기능별 대표적인 몇개의 테스트만 살펴보자.
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] 기능별 독립성을 위해서 해당 기능의 테스트를 위해 만들었던 데이터는 가상의 데이터베이스에서 지워준다.
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결과)
이제 유저가 이미 존재하는 이메일을 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메세지와 동일한 메세지를 적어줘야 테스트에 통과한다.
SNS에서 내가 다른유저에게 메세지를 보낼 때를 상상해보자. 내가 로그인하지 않은 상태에서는 다른 유저에게 메세지를 보낼 수 없다. 즉, 메세지는 로그인 한 회원의 고유 권한이다.
그래서 테스트를 위해서는 메세지 기능을 이용하기 위해서 토큰생성을 먼저한다. 그리고 request에 토큰을 실어보내어 MessageView의 데코레이터를 통과한 이후에 실재 MessageView를 테스트 할 수 있다.
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기능 테스트에서 사용하기 위해 클래스의 객체로 만들어준다.
주체가되는 유저가 여러유저와 주고받은 메세지 내용을 보여주는 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리턴값까지 의도한 데로 일치하는지는 확인 할 수 없다.
주체가 되는 유저가 한 유저와 주고받은 메세지의 히스토리를 보여주는 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번유저에게 메세지를 보낸다.
이제 주체가되는 유저가 특정유저(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와 함께 실어서 보낸다.
팔로우 기능검증에 사용될 셋업데이터를 설정해주자.
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] 로그인하지 않은 유저는 팔로우를 할 수 없기 때문에 토큰을 만들어진 유저정보를 바탕으로 임의로 만들어 클래스객체로 지정한다.
어떤유저가 어떤유저를 팔로우 했다는 사실을 데이터베이스에 저장하는 기능을 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메세지가 뜬다.
구글로그인 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를 포함한 요청을 보낸다.
오늘도 감사히 도움이 됐습ㄴㅣ다 heechul yoon