[DRF] Testing

강민성·2024년 10월 2일

DRF API Guide

목록 보기
27/28

테스트

테스트가 없는 코드는 설계부터 망가진 코드다.
— Jacob Kaplan-Moss

REST framework는 Django의 기존 테스트 프레임워크를 확장하는 몇 가지 헬퍼 클래스들을 포함하고 있으며, API 요청을 더 쉽게 지원합니다.

APIRequestFactory

Django의 기존 RequestFactory 클래스를 확장합니다.

테스트 요청 생성하기

APIRequestFactory 클래스는 Django의 표준 RequestFactory 클래스와 거의 동일한 API를 지원합니다. 즉, 표준 get(), post(), put(), patch(), delete(), head(), options() 메서드를 모두 사용할 수 있습니다.

from rest_framework.test import APIRequestFactory

# 표준 RequestFactory API를 사용하여 form POST 요청 생성
factory = APIRequestFactory()
request = factory.post('/notes/', {'title': '새로운 아이디어'})

format 인수 사용하기

post, put, patch와 같이 요청 본문을 생성하는 메서드들은 format 인수를 포함하여 multipart form data 외의 콘텐츠 타입으로 요청을 쉽게 생성할 수 있게 합니다. 예를 들어:

# JSON POST 요청 생성
factory = APIRequestFactory()
request = factory.post('/notes/', {'title': '새로운 아이디어'}, format='json')

기본적으로 사용할 수 있는 형식은 'multipart''json'입니다. Django의 기존 RequestFactory와 호환성을 유지하기 위해 기본 형식은 'multipart'입니다.

더 넓은 형식의 요청을 지원하거나 기본 형식을 변경하려면 설정 섹션을 참고하세요.

요청 본문을 명시적으로 인코딩하기

요청 본문을 명시적으로 인코딩해야 하는 경우 content_type 플래그를 설정하여 인코딩할 수 있습니다. 예를 들어:

request = factory.post('/notes/', json.dumps({'title': '새로운 아이디어'}), content_type='application/json')

양식 데이터로 PUT 및 PATCH 요청 보내기

Django의 RequestFactory와 REST framework의 APIRequestFactory 간의 차이점 중 하나는 multipart form data가 .post()뿐만 아니라 다른 메서드에서도 인코딩된다는 점입니다.

예를 들어, APIRequestFactory를 사용하여 form PUT 요청을 보낼 수 있습니다.

factory = APIRequestFactory()
request = factory.put('/notes/547/', {'title': '데이브에게 이메일 보내기'})

Django의 RequestFactory를 사용한다면 데이터를 명시적으로 인코딩해야 합니다.

from django.test.client import encode_multipart, RequestFactory

factory = RequestFactory()
data = {'title': '데이브에게 이메일 보내기'}
content = encode_multipart('BoUnDaRyStRiNg', data)
content_type = 'multipart/form-data; boundary=BoUnDaRyStRiNg'
request = factory.put('/notes/547/', content, content_type=content_type)

인증 강제 적용

요청 팩토리를 사용하여 뷰를 직접 테스트할 때, 올바른 인증 자격 증명을 구성하지 않고도 요청을 직접 인증할 수 있으면 편리합니다.

강제로 인증된 요청을 하려면 force_authenticate() 메서드를 사용하세요.

from rest_framework.test import force_authenticate

factory = APIRequestFactory()
user = User.objects.get(username='olivia')
view = AccountDetail.as_view()

# 뷰에 인증된 요청 보내기
request = factory.get('/accounts/django-superstars/')
force_authenticate(request, user=user)
response = view(request)

이 메서드의 시그니처는 force_authenticate(request, user=None, token=None)입니다. 호출 시 usertoken 중 하나 또는 둘 다 설정할 수 있습니다.

예를 들어, 토큰을 사용하여 강제로 인증하려면 다음과 같이 할 수 있습니다.

user = User.objects.get(username='olivia')
request = factory.get('/accounts/django-superstars/')
force_authenticate(request, user=user, token=user.auth_token)

참고: force_authenticate는 요청의 user를 메모리에 있는 사용자 인스턴스로 직접 설정합니다. 저장된 사용자 상태를 업데이트하는 여러 테스트에서 동일한 사용자 인스턴스를 재사용하는 경우 테스트 간에 refresh_from_db()를 호출해야 할 수 있습니다.

참고: APIRequestFactory를 사용할 때 반환되는 객체는 Django의 표준 HttpRequest이며, REST framework의 Request 객체는 뷰가 호출된 후에만 생성됩니다.

따라서 요청 객체에 속성을 직접 설정하는 것이 항상 예상한 대로 작동하지 않을 수 있습니다. 예를 들어, token을 직접 설정해도 아무런 효과가 없고, user를 설정하는 것도 세션 인증이 사용되는 경우에만 작동합니다.

# `SessionAuthentication`이 사용되는 경우에만 인증됩니다.
request = factory.get('/accounts/django-superstars/')
request.user = user
response = view(request)

CSRF 검증 강제 적용

기본적으로 APIRequestFactory로 생성된 요청은 REST framework 뷰에 전달될 때 CSRF 검증이 적용되지 않습니다. CSRF 검증을 명시적으로 켜려면 팩토리를 인스턴스화할 때 enforce_csrf_checks 플래그를 설정하면 됩니다.

factory = APIRequestFactory(enforce_csrf_checks=True)

참고: Django의 표준 RequestFactory는 이 옵션을 포함할 필요가 없습니다. Django를 사용할 때는 CSRF 검증이 미들웨어에서 이루어지지만, 뷰를 직접 테스트할 때는 미들웨어가 실행되지 않기 때문입니다. REST framework에서는 CSRF 검증이 뷰 내부에서 이루어지므로, 요청 팩토리가 뷰 수준의 CSRF 검증을 비활성화해야 합니다.

APIClient

APIClient는 Django의 기존 Client 클래스를 확장한 것입니다.

요청하기

APIClient 클래스는 Django의 표준 Client 클래스와 동일한 요청 인터페이스를 지원합니다. 즉, .get(), .post(), .put(), .patch(), .delete(), .head(), .options() 등의 표준 메서드를 모두 사용할 수 있습니다. 예를 들어:

from rest_framework.test import APIClient

client = APIClient()
client.post('/notes/', {'title': 'new idea'}, format='json')

더 넓은 요청 형식을 지원하거나 기본 형식을 변경하려면 설정 섹션을 참조하세요.

인증하기

.login(**kwargs)

login 메서드는 Django의 일반 Client 클래스와 동일하게 동작합니다. 이를 통해 SessionAuthentication을 포함하는 모든 뷰에 대해 요청을 인증할 수 있습니다.

# 로그인된 세션 컨텍스트에서 모든 요청을 만듭니다.
client = APIClient()
client.login(username='lauren', password='secret')

로그아웃하려면 일반적으로 logout 메서드를 호출하면 됩니다.

# 로그아웃
client.logout()

login 메서드는 세션 인증을 사용하는 API를 테스트할 때 적합합니다. 예를 들어, API와 AJAX 상호작용을 포함하는 웹사이트가 이에 해당합니다.

.credentials(**kwargs)

credentials 메서드는 이후 테스트 클라이언트가 수행하는 모든 요청에 포함될 헤더를 설정하는 데 사용할 수 있습니다.

from rest_framework.authtoken.models import Token
from rest_framework.test import APIClient

# 모든 요청에 적절한 `Authorization:` 헤더를 포함합니다.
token = Token.objects.get(user__username='lauren')
client = APIClient()
client.credentials(HTTP_AUTHORIZATION='Token ' + token.key)

credentials 메서드를 두 번째로 호출하면 기존의 모든 자격 증명이 덮어써집니다. 메서드를 인수 없이 호출하면 설정된 자격 증명을 제거할 수 있습니다.

# 자격 증명 포함을 중단합니다.
client.credentials()

credentials 메서드는 기본 인증, OAuth1a 및 OAuth2 인증, 간단한 토큰 인증 스키마 등 인증 헤더가 필요한 API를 테스트할 때 적합합니다.

.force_authenticate(user=None, token=None)

때로는 인증을 완전히 우회하고, 테스트 클라이언트가 수행하는 모든 요청을 자동으로 인증된 것으로 처리하고 싶을 수 있습니다.

이 방법은 API를 테스트할 때 유효한 인증 자격 증명을 만들지 않고도 테스트 요청을 수행할 수 있는 유용한 지름길입니다.

user = User.objects.get(username='lauren')
client = APIClient()
client.force_authenticate(user=user)

추후 요청에서 인증을 해제하려면 user 및/또는 tokenNone으로 설정하여 force_authenticate를 호출하세요.

client.force_authenticate(user=None)

CSRF 검증

기본적으로 APIClient를 사용할 때는 CSRF 검증이 적용되지 않습니다. CSRF 검증이 필요하다면, 클라이언트를 인스턴스화할 때 enforce_csrf_checks 플래그를 설정하여 명시적으로 활성화할 수 있습니다.

client = APIClient(enforce_csrf_checks=True)

일반적으로 세션 인증된 뷰에만 CSRF 검증이 적용됩니다. 이는 클라이언트가 login() 메서드를 호출하여 로그인된 경우에만 CSRF 검증이 발생함을 의미합니다.

RequestsClient

REST framework에는 인기 있는 Python 라이브러리인 requests를 사용하여 애플리케이션과 상호작용할 수 있는 클라이언트도 포함되어 있습니다. 이 클라이언트는 다음과 같은 경우에 유용할 수 있습니다.

  • 주로 다른 Python 서비스에서 API와 인터페이스할 것으로 예상하고, 서비스가 클라이언트에서 보이는 동일한 수준에서 테스트하려는 경우.
  • 테스트를 스테이징 또는 실제 환경에서도 실행할 수 있도록 작성하고 싶은 경우. (아래 "라이브 테스트" 참고)
  • 이는 requests 세션을 직접 사용하는 것과 동일한 인터페이스를 노출합니다.
from rest_framework.test import RequestsClient

client = RequestsClient()
response = client.get('http://testserver/users/')
assert response.status_code == 200

RequestsClient는 완전히 정규화된 URL을 전달해야 한다는 점에 유의하세요.

RequestsClient와 데이터베이스 작업

RequestsClient 클래스는 서비스 인터페이스와만 상호작용하는 테스트를 작성하려는 경우에 유용합니다. 이는 모든 상호작용이 API를 통해 이루어져야 한다는 점에서 표준 Django 테스트 클라이언트를 사용하는 것보다 조금 더 엄격합니다.

RequestsClient를 사용하는 경우, 테스트 설정 및 결과 확인을 데이터베이스 모델과 직접 상호작용하는 대신, 일반 API 호출로 수행해야 합니다. 예를 들어, Customer.objects.count() == 3와 같이 직접 확인하는 대신, 고객 엔드포인트를 나열하고 세 개의 레코드가 포함되어 있는지 확인해야 합니다.

헤더 및 인증

커스텀 헤더와 인증 자격 증명은 일반 requests.Session 인스턴스를 사용할 때와 동일한 방식으로 제공할 수 있습니다.

from requests.auth import HTTPBasicAuth

client.auth = HTTPBasicAuth('user', 'pass')
client.headers.update({'x-test': 'true'})

CSRF

SessionAuthentication을 사용 중이라면, POST, PUT, PATCH, DELETE 요청 시 CSRF 토큰을 포함해야 합니다.

JavaScript 기반 클라이언트가 사용하는 것과 동일한 흐름을 따를 수 있습니다. 먼저 CSRF 토큰을 얻기 위해 GET 요청을 보내고, 그 후 해당 토큰을 다음 요청에 전달합니다.

예시:

client = RequestsClient()

# CSRF 토큰을 얻습니다.
response = client.get('http://testserver/homepage/')
assert response.status_code == 200
csrftoken = response.cookies['csrftoken']

# API와 상호작용합니다.
response = client.post('http://testserver/organisations/', json={
    'name': 'MegaCorp',
    'status': 'active'
}, headers={'X-CSRFToken': csrftoken})
assert response.status_code == 200

라이브 테스트

RequestsClientCoreAPIClient를 신중하게 사용하면, 개발 중인 환경뿐만 아니라 스테이징 서버나 프로덕션 환경에서도 테스트 케이스를 실행할 수 있습니다.

이 스타일을 사용하여 몇 가지 핵심 기능에 대한 기본 테스트를 작성하는 것은 라이브 서비스를 검증하는 강력한 방법입니다. 이를 수행하려면 테스트가 고객 데이터를 직접적으로 영향을 주지 않도록 설정 및 해제를 신중하게 관리해야 할 수 있습니다.


CoreAPIClient

CoreAPIClient는 Python의 coreapi 클라이언트 라이브러리를 사용하여 API와 상호작용할 수 있게 해줍니다.

# API 스키마를 가져옵니다.
client = CoreAPIClient()
schema = client.get('http://testserver/schema/')

# 새 조직을 만듭니다.
params = {'name': 'MegaCorp', 'status': 'active'}
client.action(schema, ['organisations', 'create'], params)

# 조직이 목록에 존재하는지 확인합니다.
data = client.action(schema, ['organisations', 'list'])
assert(len(data) == 1)
assert(data == [{'name': 'MegaCorp', 'status': 'active'}])

헤더 및 인증

커스텀 헤더와 인증은 RequestsClient와 유사한 방식으로 CoreAPIClient에서도 사용할 수 있습니다.

from requests.auth import HTTPBasicAuth

client = CoreAPIClient()
client.session.auth = HTTPBasicAuth('user', 'pass')
client.session.headers.update({'x-test': 'true'})

API 테스트 케이스

REST framework는 Django의 기본 Client 대신 APIClient를 사용하는 아래의 테스트 케이스 클래스를 포함하고 있습니다. 이들은 기존 Django 테스트 케이스 클래스를 반영합니다.

  • APISimpleTestCase
  • APITransactionTestCase
  • APITestCase
  • APILiveServerTestCase

예시

REST framework의 테스트 케이스 클래스는 Django의 일반 테스트 케이스 클래스와 동일하게 사용할 수 있습니다. self.client 속성은 APIClient 인스턴스가 됩니다.

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):
        """
        새 계정 객체를 생성할 수 있는지 확인합니다.
        """
        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')

URLPatternsTestCase

REST framework는 클래스별로 urlpatterns를 격리할 수 있는 테스트 케이스 클래스도 제공합니다. 이 클래스는 Django의 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_create_account(self):
        """
        새 계정 객체를 생성할 수 있는지 확인합니다.
        """
        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)

응답 테스트

응답 데이터 확인하기

테스트 응답의 유효성을 확인할 때, 전체 렌더링된 응답을 검사하기보다는 응답이 생성된 데이터를 확인하는 것이 더 편리한 경우가 많습니다.

예를 들어, response.data를 검사하는 것이 더 간단합니다.

response = self.client.get('/users/4/')
self.assertEqual(response.data, {'id': 4, 'username': 'lauren'})

response.content를 파싱한 결과를 검사하는 대신:

response = self.client.get('/users/4/')
self.assertEqual(json.loads(response.content), {'id': 4, 'username': 'lauren'})

응답 렌더링

APIRequestFactory를 사용하여 뷰를 직접 테스트하는 경우, 반환된 응답은 아직 렌더링되지 않은 상태입니다. 이는 Django의 내부 요청-응답 사이클에서 템플릿 응답이 렌더링되기 때문입니다. response.content에 접근하려면 먼저 응답을 렌더링해야 합니다.

view = UserDetail.as_view()
request = factory.get('/users/4')
response = view(request, pk='4')
response.render()  # 이 메서드를 호출하지 않으면 `response.content`에 접근할 수 없습니다.
self.assertEqual(response.content, '{"username": "lauren", "id": 4}')

설정

기본 형식 설정

기본적으로 테스트 요청에 사용되는 형식은 TEST_REQUEST_DEFAULT_FORMAT 설정 키를 사용하여 설정할 수 있습니다. 예를 들어, 표준 멀티파트 폼 요청 대신 테스트 요청에 기본적으로 JSON을 사용하도록 하려면 settings.py 파일에서 다음과 같이 설정할 수 있습니다.

REST_FRAMEWORK = {
    ...
    'TEST_REQUEST_DEFAULT_FORMAT': 'json'
}

사용 가능한 형식 설정

multipart 또는 JSON 요청이 아닌 다른 형식을 사용하여 요청을 테스트해야 하는 경우, TEST_REQUEST_RENDERER_CLASSES 설정을 사용하여 형식을 추가할 수 있습니다.

예를 들어, 테스트 요청에서 format='html'을 사용하도록 지원하려면, settings.py 파일에 다음과 같이 설정할 수 있습니다.

REST_FRAMEWORK = {
    ...
    'TEST_REQUEST_RENDERER_CLASSES': [
        'rest_framework.renderers.MultiPartRenderer',
        'rest_framework.renderers.JSONRenderer',
        'rest_framework.renderers.TemplateHTMLRenderer'
    ]
}

Reference

DRF API Guide - Testing

profile
Back-end Junior Developer

0개의 댓글