Search

Django Test

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

model_bakery와 달리 사람 친화적인 랜덤 데이터 생성 가능
→ 단 필드마다 적절한 faker provider 지정해야 함
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