우리는 API를 개발한 후 기능이 정상 작동하는지 체크하기 위해 수동 또는 자동으로 작업을 진행합니다.
수동으로는 크게 POSTMAN
을 이용해 작업을 진행했는데 API가 별로 없으면 괜찮지만 수없이 많고 코드를 수정할 때마다 수작업으로 하기에는 시간적으로나 비용적으로나 크게 소모가 된다고 생각이 들었습니다. 그리고 API 문서를 만들어 놓더라도 보고 테스트하는 사람마다 똑같이 수행하기가 어렵다는 것을 느꼈고 여기서 코드로 구성을 해놔야만 하겠다 생각했습니다.
그래서 자동으로 하는 방법을 찾아보았습니다.
되게 다양한 방법이 존재하겠지만 저는 pytest
를 간략하게 사용해보고, DRF Test
를 이용해 본격적으로 코드를 구성했습니다.
다음은 DRF 공식문서에 명시되어 있는 코드입니다. 한 줄씩 천천히 봐보면 다음과 같습니다.
from django.urls import reverse
from rest_framework import status
from rest_framework.test import APITestCase
from myproject.apps.core.models import Account
class AccountTests(APITestCase):
def test_create_account(self):
"""
Ensure we can create a new account object.
"""
url = reverse('account-list')
data = {'name': 'DabApps'}
response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(Account.objects.count(), 1)
self.assertEqual(Account.objects.get().name, 'DabApps')
DRF Test를 이용하기 위해서는 우선 Import를 진행해주어야 합니다. 그 후 클래스 생성 후 APITestCase를 상속시켜줍니다.
from rest_framework.test import APITestCase
class AccountTests(APITestCase):
.
.
.
함수명은 test_ 로 시작하게 작성해줍니다. 그래야 test 함수를 인식하고 테스트를 진행할 수 있습니다.
def test_create_account(self):
URL을 작성하는 방법은 2가지가 있습니다.
--- 1. 첫 번째 방법 ---
url = reverse('account-list')
--- 2. 두 번째 방법 ---
url = "/cert/loginSync/"
추가로 URL은 전체 도메인이 아닌 URL의 경로를 지정해주어야 합니다.
>>> c.get('/login/') ⭕
>>> c.get('https://www.example.com/login/') ❌
Test Client는 크게 2개의 인자를 받습니다.
우선 첫 번째로 url입니다. 위의 URL 규칙을 따라 작성해주면 됩니다.
두 번째로 data입니다. 파라미터로 넘길 데이터가 존재한다면 data에 담아서 같이 보내주면 됩니다.
data = {'name': 'DabApps'}
response = self.client.post(url, data, format='json')
response는 크게 status_code, format으로 반환됩니다. 하지만 API 반환 값으로 특정 데이터를 담아 보냈다면 확인하는 방법이 존재합니다.
다음과 같이 response를 json화 시켜주면 내부의 값을 원하는 대로 확인하고 테스트 할 수 있습니다.
data = response.json()
대표적으로 assertEqual
가 있습니다. 왼쪽 값과 오른 쪽 값을 비교해 다르다면 에러를 발생시키는 함수인데요
assert ~
로 시작하는 함수들은 크게 3개의 인자를 받습니다.
첫 번째와 두 번째 인자는 값을 비교할 대상을 넣어주면 됩니다. 마지막 3번째 인자로 두 개의 인자가 값이 다를 때 나타낼 메세지를 넣어주면 됩니다.
def assertEqual(self, first, second, msg=None):
"""Fail if the two objects are unequal as determined by the '=='
operator.
"""
assertion_func = self._getAssertEqualityFunc(first, second)
assertion_func(first, second, msg=msg)
다음은 쓰임을 한 번 보면 다음과 같습니다.
self.assertEqual(response.status_code, status.HTTP_201_CREATED, "값이 다를 때 나오는 message")
self.assertEqual(Account.objects.count(), 1, "값이 다를 때 나오는 message")
self.assertEqual(Account.objects.get().name, 'DabApps', "값이 다를 때 나오는 message")
assertEqual
뿐만 아니라 그 외의 함수가 다양하게 있으니 한 번 찾아보시고 사용하는 것을 권해드립니다.
다음과 같이 실행시켜주면 됩니다.
python manage.py test [폴더가 있으면 폴더명]
추가로 유용한 명령들이 많은데 대표적으로 유용하게 썻던 2가지를 설명드릴게요
verbosity는 자세하게 표현해주는 것을 의미하는데요. 총 4단계로 표현해주고 있습니다.
0 - 최소 출력
1 - 정상 출력
2 - 자세한 출력
3 - 매우 자세한 출력
사용 법은 다음과 같습니다.
python manage.py test [폴더가 있으면 폴더명] -v 3
python manage.py test [폴더가 있으면 폴더명] --verbosity 3
-b, --buffer는 통과한 테스트의 출력을 버립니다. 사용 법은 위처럼 뒤에다 붙여 적어주면 됩니다.
하나의 클래스 내에 정의된 각각의 테스트 메소드들에 대해 setUp은 테스트 메소드 실행 전에 실행되고, tearDown은 테스트 메소드 실행 후에 실행됩니다.
매 번 객체가 새롭게 생성되므로 꼭 필요한 경우가 아니라면 사용하지 않는 것을 권장합니다. 필요한 테스트 함수 내에서 필요한 객체만 생성하는 것이 효율적이고 빠릅니다.
하나의 테스트 클래스 내에 정의된 테스트 메소드 실행 시 전체 1번만 동작합니다.
하나의 모듈 내에서 전체 1번만 동작합니다.
저는 DRF Toekn authentication을 구현해 API를 구성했습니다. 그래서 Token 권한을 필요로 하는 곳에 아래와 같이 진행했는데 인식을 제대로 하지 못하는 에러를 겪었습니다.
- setUp에서 회원가입 로직을 태워 유저 및 토큰을 생성
- 생성받은 토큰을 전역변수 토큰에 넣어주고
- 토큰을 필요로 하는 API에 헤더로 토큰을 넣어줘서 테스트 진행
대표적으로 아래의 두 가지 에러를 겪었는데요, 정확하게 왜 그런지 아직까지 이유를 찾지 못했습니다.
1. AttributeError: 'TestCert' object has no attribute 'user'
2. Django doesn't provide a DB representation for AnonymousUser.
그래도 테스트는 해야하니 다른 방법을 찾았습니다. 그래서 찾은 방법은 강제 권한 인증 방법입니다.
유저를 생성하고 마지막에 있는 유저를 기준으로 토큰 강제 인증을 진행한 방법입니다.
user = User.objects.all().last()
self.client.force_authenticate(user=user)
이러면 User에 해당하는 Token을 따로 넣어주지 않아도 인증이 된 상태로 넘어가 API Test가 가능합니다.
처음 pytest, DRF Test 등 테스트 코드를 작성하면서 느꼈던 건데 GET
메소드를 이용해 보내게 되면 정보가 하나도 넘어오지 않는 점을 가지고 많이 헤맸습니다. POST
로 보내는 거는 가는데 왜 GET
은 안가지?
알고 봤더니 테스트 시작과 마지막에 test_db
를 생성하고 지운다는 것을 알 수 있었습니다. 덕분에 어떤 데이터를 불러오는 과정에서 확인하고 싶다면 먼저 POST
로 던져주고 확인해야 된다는 점도 그제야 알았구요
이미 존재하는 데이터를 가지고 확인해 주면 안되나라는 생각을 처음에 했었는데 계속해서 테스트를 진행해 보니 쓰고 지우고 하는 데이터가 방대한데 이거를 개발 DB에 적용시켰다면 부하가 어마했을 거라고 생각하고 한 번 더 DRF에 감탄했습니다~
DRF Test는 테스트 함수 내 에러가 발생하면 끝까지 탐색하지 않고 그 즉시 멈춰버린다는 단점을 가지고 있습니다.
Pytest는 병렬 처리가 가능해서 끝까지 탐색하고 어떤 부분이 성공했고 실패했는지 전체적으로 보여준다는 점에 비해서는 단점이라고 생각합니다.
소프트웨어 테스트를 논할 때 얼마나 테스트가 충분한가를 나타내는 지표 중 하나입니다. 말그대로 코드가 얼마나 커버가 되었는지를 확인할 수 있습니다.
1. coverage를 설치해줍니다.
pip install coverage
2. 리포트를 보기 전에 run을 먼저 실행합니다.
coverage run manage.py test [폴더]
3. 그 후 report를 보면 어느정도 테스트 코드가 coverage 되었는지 확인 가능합니다.
coverage report
3. .py 별로 어느정도의 커버리지가 되었는지 확인할 수 있습니다.
다음의 명령을 실행해 html 파일을 만들어주고 실행시켜주면 자세하게 확인할 수 있습니다.
coverage html
파이썬 장고의 단위 테스트를 구성해 사용하던 중 성공했던 Case들이 TransactionManagementError
를 발생하며 지속적으로 실패되는 문제를 겪었습니다.
해당 TestCase는 테스트 함수들이 DB에 대한 질의를 수행하는 코드를 호출하며 발생하였습니다. 이러한 현상은 다른 TestCase가 실패했을 경우, 기존에 성공했던 TestCase에서 발생한 현상으로써 왜 나타나는지 정확히 이해하지 못했습니다.
raise TransactionManagementError(
django.db.transaction.TransactionManagementError: An error occurred in the current transaction. You can't execute queries until the end of the 'atomic' block.
공식 문서를 살펴보니 TransactionTestCase
도 지원하고 있습니다.
저는 DRF Test 패키지의 APITestCase
클래스를 사용했습니다. 이 클래스를 APITransactionTestCase
로 변경하니 에러가 말끔히 사라지고 테스트도 정상적으로 수행되었습니다.
# 기존
from rest_framework.test import APITestCase
# 변경
from rest_framework.test import APITransactionTestCase
참고 자료 📩
Django Docs - Test 하는 방법
DRF Docs - APITestCase
DRF Docs - authentication
python assertation 종류