1. Test Basic
가. Running Test
1) 테스트 실행
•
python manage.py test
2) 테스트 실행 with 로그 확인
•
python manage.py test --verbosity 2
3) PyTest 테스트 실행
•
pytest
•
Addopts: 매번 pytest 명령 실행에 따라 적용되는 PyTest 명령 명시
→ -vv: 실패한 assertion에 대해 추가적인 정보 제공
→ -x: 실패한 테스트가 있다면 나머지 테스트 실행 취소
→ --lf: 가장 최근 실행한 테스트 중 실패한 테스트부터 테스트 실행
→ --cov: test coverage 출력
[pytest]
DJANGO_SETTINGS_MODULE = config.settings
python_files = tests.py test_*.py *_tests.py
addopts = -vv -x --lf --cov
Plain Text
복사
•
marking된 테스트 실행
ex)pytest -m e2e orpytest -m unit
2. DRF Test Docs
가. API Request Factory
Django's RequestFactory class 확장판
1) RequestFactory class란
•
실 요청에 전달되는 매개변수를 토대로 request 인스턴스 반환
→ 반환된 request 인스턴스로 클래스 뷰, 함수 뷰에 매개변수로 전달하여 테스트
2) Authentication
•
force_authenticate() 호출에 따라 요청에 대해 직접 인증하여 편리하게 작업 가능
user = User.objects.get(username='olivia')
request = factory.get('/accounts/django-superstars/')
force_authenticate(request, user=user, token=user.auth_token)
Python
복사
3) APIRequestFactory vs RequestFactory
•
content_type이 multipart/form_data일 경우
→ RequestFactory는 PUT과 PATCH의 경우 인코딩 방식을 명시적으로 정의해야함
→ APIRequestFactory의 경우 인코딩 방식을 명시적으로 정의할 필요가 없음
# APIRequestFactory
factory = APIRequestFactory()
request = factory.put('/notes/547/', {'title': 'remember to email dave'})
# RequestFactory
factory = RequestFactory()
data = {'title': 'remember to email dave'}
content = encode_multipart('BoUnDaRyStRiNg', data)
content_type = 'multipart/form-data; boundary=BoUnDaRyStRiNg'
request = factory.put('/notes/547/', content, content_type=content_type)
Python
복사
나. API Client
Django's Client class 확장판
1) Client class란
•
가짜 Web Browser처럼 동작하므로 특정 요청에 따라 웹 브라우저의 일련의 동작에 대해 테스트 가능
→ GET, POST 요청에 따른 응답결과 테스트 가능
→ 특정 요청에 대한 일련의 Redirect 과정 테스트 가능
→ 템플릿에 의해 렌더링된 결과 테스트 가능
2) Authentication
•
credentials(): authentication headers를 요구하는 API 테스트에 적절
→ ex) OAuth2, OAuth1a, basic authentication
from rest_framework.authtoken.models import Token
from rest_framework.test import APIClient
# Include an appropriate `Authorization:` header on all requests.
token = Token.objects.get(user__username='lauren')
client = APIClient()
client.credentials(HTTP_AUTHORIZATION='Token ' + token.key)
Python
복사
•
login(): session authentication을 요구하는 API 테스트에 적절
# Make all requests in the context of a logged in session.
client = APIClient()
client.login(username='lauren', password='secret')
Python
복사
다. Testing Response
1) Checking Response Data
•
응답값 자체에 포함된 데이터를 대상으로 데이터 검사 권장
→ 완전하게 렌더링된 응답 내 데이터를 대상으로 데이터 검사 X
# 권장
response = self.client.get('/users/4/')
self.assertEqual(response.data, {'id': 4, 'username': 'lauren'})
# 지양
response = self.client.get('/users/4/')
self.assertEqual(json.loads(response.content), {'id': 4, 'username': 'lauren'})
Python
복사
2) APIRequestFactory 사용시 주의사항
•
APIRequestFactory 사용에 따라 response.content를 사용할 경우, render() 메소드 호출해야 함
view = UserDetail.as_view()
request = factory.get('/users/4')
response = view(request, pk='4')
response.render() # Cannot access `response.content` without this.
self.assertEqual(response.content, '{"username": "lauren", "id": 4}')
Python
복사
라. Test Cases
1) Django Test Cases
•
장고에서 제공하는 네 가지 Test Cases는 다음과 같은 관계를 가짐
•
SimpleTestCase
→ 가장 기본적인 테스트 케이스
→ 쿼리를 사용하는 테스트를 기본적으로 허용 X
•
TransactionTestCase
→ SimpleTestCase에 쿼리를 사용하는 테스트 추가
→ 각각의 테스트 실행 후 DB를 최초의 상태로 롤백
→ 각각의 테스트가 완료됨에 따라 테이블에 변경사항이 발생한 것을 되돌려 놓음
•
TestCase
→ TransactionTestCase에서 테스트 완료 후 DB 원복하는 부분에 차이 존재
→ TestCase의 경우, class와 각각의 테스트케이스를 atomic()으로 감싸고 테스트 종료에 따라 롤백됨
→ 테스트 완료 후 DB를 원복하는 부분에서 TransactionTestCase 보다 속도가 빠르기 때문에 일반적으로 많이 사용됨
→ 트랜잭션 내부에서 발생하는 로직에 대해 테스트 불가(이런 경우 TransactionTestCase 활용)
→ 쿼리 중간에 예외를 검증하는 로직이 포함된 테스트의 경우, TransactionTestCase 활용
•
LiveServerTestCase
→ 테스트 셋업 시 live django server를 올리고 테스트 종료 시 서버를 내리는 식으로 테스트를 운용함
→ dummy client가 아니라 automated test client를 사용할 수 있음
1) DRF Test Case
•
DRF Test Case는 장고에서 제공하는 네 가지 Test Case를 사용함
→ DRF Test Case 사용 시 client 객체는 DRF에서 제공하는 API Client를 사용해야 함
→ APISimpleTestCase ,APITransactionTestCase ,APITestCase ,APILiveServerTestCase
•
예시
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')
Python
복사
2) URL Patterns Test Case
•
클래스 기반으로 나뉘어지는 URL별로 테스트 케이스 생성
→ Django's SimpleTestCase만을 상속함
→ 일반적으로 다른 테스트케이스클래스와 함께 사용됨
from django.urls import include, path, reverse
from rest_framework.test import APITestCase, URLPatternsTestCase
class AccountTests(APITestCase, URLPatternsTestCase):
urlpatterns = [
path('api/', include('api.urls')),
]
def test_retrieve_account(self):
"""
Ensure we can create a new account object.
"""
url = reverse('account-list')
response = self.client.get(url, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(len(response.data), 1)
Python
복사
3. Testing Frameworks
가. PyTest General
1) PyTest 설정
•
package 설치
→ pytest, model_bakery
•
mysql-client 설치 이슈
→ mac m1에 pip로 mysql-client 설치 이슈 발생
→ brew install mysql
→ brew install mysqlclient
•
pytest.ini 설정
→ 코드 참고
→ settings.py 파일이 아래와 같은 경로에 있을 경우, DJANGO_SETTINGS_MODULE의 설정은 다음과 같음(settings.py 경로: drf-tutorial/config/settings.py)
[pytest]
DJANGO_SETTINGS_MODULE = config.settings
python_files = tests.py test_*.py *_tests.py
Python
복사
2) fixture
•
fixture: 테스트를 간결하게 작성하기 위해 공통적으로 필요한 부분을 미리 작성한 코드
@pytest.fixture
def api_client():
return APIClient
def test_list(self, api_client):
Python
복사
3) Marker
•
@pytest.mark.parametrize(): 첫번째 매개변수로 전달된 변수에 두번째 매개변수로 전달한 리스트 내 가각의 값들이 순차적으로 들어감
→ 아래의 예시 코드의 경우, field 변수에 각각 code, name, symbol 값이 대입되면서 같은 테스트가 3번 실행됨
@pytest.mark.parametrize('field', [
('code'),
('name'),
('symbol'),
])
def test_partial_update(self, mocker, rf, field, api_client):
currency = baker.make(Currency)
currency_dict = {
'code': currency.code,
'name': currency.name,
'symbol': currency.symbol
}
valid_field = currency_dict[field]
url = f'{self.endpoint}{currency.id}/'
response = api_client().patch(
url,
{field: valid_field},
format='json'
)
assert response.status_code == 200
assert json.loads(response.content)[field] == valid_field
Python
복사
•
@pytest.mark.django_db: DB에 접근하는 Integration 테스트에만 사용됨
→ 기본적으로 pytest의 테스트는 DB에 접근하지 못하는 것이 기본값임
→ 아래 예시와 같이 pytestmark 변수에 pytest.mark.django_db marker를 할당하면 해당 파일의 모든 테스트는 DB 접근이 허용됨
pytestmark = pytest.mark.django_db
Python
복사
•
특정 marker 테스트만 실행
→ pytest -m <marker>
→ 예시
# @pytest.mark.parametrize 달린 테스트만 실행
pytest -m parametrize
Plain Text
복사
•
custom marker: test를 그룹화할 때 사용 가능
→ 특정 마커만 달린 테스트만 그룹화하여 실행
# pytest.ini
[tool:pytest]
markers =
# Define our new marker
unit: tests that are isolated from the db, external api calls and other mockable internal code.
Plain Text
복사
•
custom marker를 테스트 케이스에서 사용 시 다음과 같이 pytestmark 변수에 할당하여 사용 가능
# (imports)
# Only one global marker (most commonly used)
pytestmark = pytest.mark.unit
# Several global markers
pytestmark = [pytest.mark.unit, pytest.mark.other_criteria]
# (tests)
Plain Text
복사
4) Mocking
•
Unit Test에서 활용
나. Factory
1) Factory 기본
•
Facotory: 가짜 인스턴스 모델 생성 도구
•
persistent instance or non persistent instance로 선택적 사용 가능
→ persistent instance: 생성된 가짜 모델 인스턴스를 DB에 저장 O, Integration Test에 사용됨
→ non persistent instance: 생성된 가짜 모델 인스턴스를 DB에 저장 X, Unit Test에 사용됨
2) model_bakery
•
Django Model의 제약사항에 따라 유효한 데이터로 채워진 가짜 모델 인스턴스 제공
•
사람 친화적이지 않은 랜덤 데이터로 값이 채워짐
→ 필드마다 사람 친화적인 데이터 생성을 고려하지 않아도 됨으로 상대적으로 생산성이 높음
•
non persistent instance 인스턴스 생성
bakery import baker
from apps.my_app.models import MyModel
# create and don't save
baker.prepare(MyModel) # --> One instance
baker.prepare(MyModel, _quantity=3) # --> Batch of 3 instances
Python
복사
code from https://dev.to/sherlockcodes/pytest-with-django-rest-framework-from-zero-to-hero-8c4
•
persistent instance 인스턴스 생성
# create and save to the database
baker.make(MyModel) # --> One instance
baker.make(MyModel, _quantity=3) # --> Batch of 3 instances
Python
복사
code from https://dev.to/sherlockcodes/pytest-with-django-rest-framework-from-zero-to-hero-8c4
3) factory_boy
•
•
non persistent instance 생성
non persistent instance의 경우 DB에서 자동으로 생성되는 값이 생성될 수 없음 ex) id, pk, fk
# factories.py
import factory
class MyModelFactory(factory.DjangoModelFactory):
class Meta:
model = MyModel
field1 = factory.faker.Faker('relevant_generator')
...
# test_something.py
# Do not save to db
MyModelFactory.build() # --> One instance
MyModelFactory.build_batch() # --> Batch of 3 instances
Python
복사
code from https://dev.to/sherlockcodes/pytest-with-django-rest-framework-from-zero-to-hero-8c4
•
persistent instance 생성
# test_something.py
# Save to db
MyModelFactory() # --> One instance
MyModelFactory.create_batch(3) # --> Batch of 3 instances
Python
복사
code from https://dev.to/sherlockcodes/pytest-with-django-rest-framework-from-zero-to-hero-8c4
다. PyTest 활용
1) E2E Tests
•
List API
→ factory instance N개 생성
→ HTTP 상태코드와 N개 생성 개수 확인
•
Retrieving API
→ model 인스턴스의 속성을 딕셔너리 객체로 전환시 '_state' 속성 자동 삽입되므로 제거 필요
new_transaction = Transaction.objects.last()
expected_json = new_transaction.__dict__
expected_json.pop('_state')
Python
복사
→ datetime 타입의 값을 json 객체에서 사용 시 str 타입으로 전환 필요
expected_json['creation_date'] = expected_json['creation_date'].strftime(
'%Y-%m-%dT%H:%M:%S.%fZ'
)
Python
복사
•
Partial Updating API
→ @pytest.mark.parametrize 활용하여 부분 수정 대상 모든 필드의 정상 수정여부 확인
•
Updating API
→ 두 개의 factory instance 생성
# 하나는 persistant instance로 관리, 다른 하나는 non persistant instance로 관리
# non persistant instance의 값으로 persistance instance에 대한 수정 가능여부 확인
old_currency = baker.make(Currency)
new_currency = baker.prepare(Currency)
or
# 두 개 모두 persistant instance로 관리
# 둘 중 하나의 인스턴스를 다른 인스턴스의 값으로 수정 가능여부 확인
old_transaction = utbb(1)[0]
new_transaction = utbb(1)[0]
Python
복사
•
Creating API
→ Github 예시 코드 참고
•
Deleting API
→ Github 예시 코드 참고
2) Unit Tests - ViewSet
•
ViewSet Unit Test 중점사항
→ Non Persistent Object 생성(or Mocking)하여 DB 의존성 제거 여부
# Mocking
mocker.patch.object(
CurrencyViewSet, 'get_queryset', return_value=qs
)
Python
복사
→ DB에서 자동 생성되는 필드(ex. id)에 대한 Util 객체 생성하여 모든 필드 대상 테스트 수행 여부
3) Unit Tests - Serializer
•
Serializer Unit Test 중점사항
→ Model Instance에 대한 Serializing의 적절 여부
currency = CurrencyFactory.build()
serializer = CurrencySerializer(currency)
assert serializer.data
Python
복사
→ valid serialized data에 대한 Deserializing의 적절 여부
valid_serialized_data = factory.build(
dict,
FACTORY_CLASS=CurrencyFactory
)
serializer = CurrencySerializer(data=valid_serialized_data)
assert serializer.is_valid()
Python
복사
Reference
•
Django Test Document, https://docs.djangoproject.com/en/3.2/topics/testing/
•
DRF TEST Helpers Document, https://www.django-rest-framework.org/api-guide/testing/
•
Django's Test Cases, https://docs.djangoproject.com/en/3.1/topics/testing/tools/#transactiontestcase
•
TransactionTestCase vs TestCase ,https://stackoverflow.com/questions/44450533/difference-between-testcase-and-transactiontestcase-classes-in-django-test
•