소프트웨어 개발에서 실제 기능 배포 이전에 해당 기능이 요구사항에 맞게 동작하는지 확인하는 것을 테스트라고 합니다. 테스트는 테스트의 범위에 따라 크게 다음과 같이 분류합니다.
- Acceptance Test (인수 테스트)
- Stress/load Test (부하 테스트)
- Function Test (기능 테스트)
- Integration Test (통합 테스트)
- Unit Test (단위 테스트)
또한 UI Test / Integration Test / Unit Test 로도 분류할 수 있는데, 이 중 UnitTest는 작성한 코드의 가장 작은 단위인 Method 나 Function을 테스트하는 것입니다. UnitTest는 가장 기본이 되는 테스트로 전체 테스트의 70%를 차지하는 비중이 큰 테스트입니다.
여태까지는 Postman을 활용하여 API의 기능들을 테스트 했었는데 이는 Integration 테스트에 해당됩니다.
UI Test에 비해서는 덜 까다롭지만 만약 테스트를 진행할 API가 많은 경우라면 시간 비용이 많이 소모되므로 비교적 자동화하기 가장 쉽고, 효과가 좋은 UnitTest를 진행해보려고 합니다.
Django에서는 python의 UnitTest를 활용할 수 있는 라이브러리가 존재합니다.
App을 만들 때, tests.py라는 파일이 생성되며, 이 곳에서 test 코드를 작성하여 UnitTest를 진행할 수 있습니다.
setUp()
메서드를 활용하여 새로운 데이터를 생성하여 테스트를 실행해야 합니다.tearDown()
메서드를 활용하여 실행한 결과를 삭제합니다. (Clean up)test_
로 시작해야 합니다.# <app name>/tests.py
from django.test import TestCase, Client
# Client : 서버를 실제로 실행하지 않고도 클라이언트 입장에서 request를 보낼 수 있습니다.
...
class SignupTest(TestCase):
def setUp(self):
... # 테스트를 위한 DB 생성 및 데이터 추가
def tearDown(self):
... # 테스트 DB 삭제
def test_signup_post_success(self):
...
self.assertEqual()
# 테스트 하려는 views.py의 결과값과 기대값을 설정할 수 있습니다.
# 주로 response의 status code, JsonResponse 객체를 기대값으로 설정합니다.
가장 기본적인 사용자 관련 API의 테스트를 진행해보고자, 이전 Swagger를 적용해보는 프로젝트를 clone하여 Unit Test를 진행 해보았습니다.
tests.py의 실행은
python3 manage.py test
로 실행할 수 있습니다.
실행결과response의 status_code가 201이 반환되어야 하지만 404가 반환되어 테스트가 실패한 것을 확인할 수 있습니다.
그 이유는 404 KeyNotFound 에러를 통해 힌트를 얻을 수 있습니다. url이 올바르지 않아 회원가입 API가 정상적으로 작동되지 않은 것입니다. 이러한 실수를 줄이기 위해 reverse()
함수를 사용하여 url을 올바르게 적용했습니다.
URL reverse()
는 url이 변경되더라도 url name을 통해 변경된 url을 추적해주는 함수입니다.from django.urls import reverse ... reverse('signup')
url을 올바르게 적용한 후, 다시 실행한 결과 다음과 같은 결과를 확인했습니다.
response를 통해 받은 status_code가 201이 아닌 200인것을 확인 할 수 있었습니다.
치명적인 에러는 아니지만 보통의 경우 로그인, 회원가입의 POST요청 결과 status_code는 201 Created
가 반환되어야 합니다. 하지만 그렇지 않았던 것을 확인했고, views.py
로직에서 status_code 201을 반환하도록 변경할 수 있었습니다.
로직 변경 결과 test를 통과할 수 있었고, 최종 회원가입API의 테스트 코드는 다음과 같이 작성되었습니다.
#회원가입 기능 관련 테스트케이스
class SignupTest(TestCase):
# 테스트에 필요한 객체를 미리 생성
def setUp(self):
pass
# 테스트 과정 중에 생긴 데이터 제거
# 다른 테스트와 연관성이 있어도 정확한 테스트 결과를 위해 Clean UP을 진행
def tearDown(self):
User.objects.all().delete()
# 회원가입 기능이 성공적으로 동작하는지 테스트
def test_signup_post_success(self):
client = Client()
signup_info = {
"username" : "signTestUser",
"password" : "test1234",
"nickname" : "테스트사용자"
}
# json.dumps()로 객체를 json화 시켜줌
response = client.post(reverse('signup'), json.dumps(signup_info), content_type='application/json')
# assertEqual()로 response 받은 status code가 정상적인지 확인
self.assertEqual(response.status_code, 201)
로그인 API 테스트는 회원가입 API 테스트와 달리 사용자 모델이 생성되어 있어야 합니다.
따라서 setUp()
을 다음과 같이 설정할 수 있습니다.
# 로그인 기능 관련 테스트케이스
class LoginTest(TestCase):
def setUp(self):
self.url = reverse('login')
self.user = User.objects.create(
username="loginTestUser",
nickname="로그인테스트사용자"
)
self.user.set_password("test0987")
self.user.save()
setUp()
을 통해 username
이 "loginTestUser"인 사용자를 생성하고, 닉네임과 비밀번호를 설정했습니다. 또한 비밀번호는 set_password
를 이용해 평문이 아닌 암호화를 진행했습니다.
로그인 API의 전체 코드는 다음과 같이 작성했습니다.
# 로그인 기능 관련 테스트케이스
class LoginTest(TestCase):
def setUp(self):
self.url = reverse('login')
self.user = User.objects.create(
username="loginTestUser",
nickname="로그인테스트사용자"
)
self.user.set_password("test0987")
self.user.save()
def tearDown(self):
User.objects.all().delete()
# 로그인 기능 테스트
def test_login_post_sucess(self):
login_info = {
"username" : "loginTestUser",
"password" : "test0987"
}
response = self.client.post(self.url, data=login_info, format='json')
self.assertEqual(response.status_code, 201)
# 로그인시 아이디가 틀렸을 경우 에러 테스트
def test_login_post_invalid_username(self):
login_info = {
"username" : "logTestUser",
"password" : "test0987"
}
response = self.client.post(self.url, data=login_info, format='json')
self.assertEqual(response.status_code, 400)
# 로그인시 비밀번호가 틀렸을 경우 에러 테스트
def test_login_post_invalid_password(self):
login_info = {
"username" : "loginTestUser",
"password" : "test1234"
}
response = self.client.post(self.url, data=login_info, format='json')
self.assertEqual(response.status_code, 400)
setUp()
에서 테스트 데이터를 생성했기 때문에 tearDown()
을 통해 테스트가 끝나면 데이터가 삭제되도록 Clean up을 했습니다. 또한 테스트 함수는 다음과 같이 세분화하여 작성해보았습니다.
- test_login_post_sucess : 로그인 기능이 제대로 동작하는지 확인하는 함수
- test_login_post_invalid_username :
username
이 틀렸을 경우 로직의 동작을 확인하는 함수- test_login_post_invalid_password :
password
가 틀렸을 경우 로직의 동작을 확인하는 함수
tests.py를 전체 실행할 수 있지만, 각기 다른 테스트 함수도 개별적으로 실행가능합니다.
python3 manage.py test <App이름>.<테스트파일 이름>.<class이름>.<test함수이름>
python3 manage.py test practise.tests.LoginTest.test_login_post_sucess
사용자 정보 조회 API의 작동을 위해서는 DRF-JWT의 access_token이 header에 있어야 사용자의 로그인 정보가 인증되어 올바른 결과를 반환 할 수 있습니다.
따라서 DRF에서 작동하는 JWT 토큰 인증방식의 API를 테스트하기 위해, Django 기본 테스트 클라이언트 클래스인 Client()
가 아닌 DRF 테스트 클라이언트 APIClient()
를 적용했습니다.
from rest_framework.test import APIClient
또한 header에 access_token을 담아 인증하기 위해 JavaScript의 credentials()
을 활용하여 HTTP_Authorization에 Bearer 인증 방식을 테스트함수에 추가해줬습니다.
credentials()
은 사용자의 자격증명을 요청하고, 저장된 자격증명을 반환하는 등의 작업을 수행하는 메서드입니다. 주로 사용자 인증 및 로그인 프로세스에서 사용됩니다.
전체 코드는 다음과 같습니다.
class UserInfoGetTest(TestCase):
"""
1. json 객체와 status=200 정상적으로 반환되는지 테스트
2. 토큰이 없을 경우 TokenErrorException class 호출에 따른 json message와 status=401 반환 테스트
"""
def setUp(self):
self.client = APIClient(enforce_csrf_checks=True)
self.maxDiff = None
self.user = User.objects.create(
id = 1,
username = "infoTestUser",
nickname = "GET테스트사용자",
profile = None
)
self.user.set_password('asdf1234')
self.user.save()
def tearDown(self):
User.objects.all().delete()
# 사용자 정보 조회 테스트
def test_userinfo_success(self):
login_user = {
"username" : "infoTestUser",
"password" : "asdf1234",
}
# 사용자 로그인
login_response = self.client.post(reverse('login'),
data=login_user, format='json')
# 로그인한 사용자의 access_token을 header에 인증
self.client.credentials(
HTTP_AUTHORIZATION=f"Bearer {login_response.json()['access_token']}"
)
# 사용자 정보 조회 response
response = self.client.get(reverse('user-info'), content_type='application/json')
# response status code 200 검증
self.assertEqual(response.status_code, 200)
# response json 객체 검증
self.assertEqual(response.json(),{
'id' : 1,
'nickname' : 'GET테스트사용자',
'profile' : None
})
# 토큰이 없는 경우
def test_userinfo_nottoken(self):
self.client.credentials(
HTTP_AUTHORIZATION=f"Bearer {''}"
)
response = self.client.get(reverse('user-info'), content_type='application/json')
self.assertEqual(response.status_code, 401)
self.assertEqual(response.json(), { 'detail' : 'unauthenticated'})
APIClient(enforce_csrf_checks=True)
를 통해 CSRF 보호기능을 활성화 해줍니다.self.maxDiff
속성을 사용하여 테스트 케이스에서 예상 결과와 실제 결과의 비교를 보다 효율적으로 관리하고, 필요한 경우에는 제한된 차이만을 출력할 수 있습니다.글에서 작성한 API 테스트 이외에도 로그인 세션유지를 위한 API UnitTest 코드 등 전체 코드는 github에 올려 두었습니다.
https://github.com/wodnrP/UnitTest_practise
기본적인 UnitTest 코드를 작성해보며 익숙하진 않지만 기본 Postman 테스트에 비해 훨씬 빠르다는 것을 느꼈습니다. 그리고 테스트 코드 작성 중 로직의 오류를 발견할 수 있어서 다시 한번 테스트 코드 작성의 중요성을 깨닫게 되었습니다. 이전보다 테스트에 많은 관심을 가지게 되었고, 개발 전 테스트 코드 작성을 통해 개발을 진행하는 TDD에 대해서도 흥미를 느껴 찾아보게 되었습니다.
작성한 글에서 부족하거나 잘못된 점이 있다면 언제든지 댓글 달아주시면 감사하겠습니다.
질문, 의견 모두 언제나 환영입니다.