
Validation(검증)
중복 투표 방지
같은 question에 대해 동일한 voter가 여러 번 투표하는 것을 방지하기 위해 UniqueTogetherValidator를 사용합니다.
# polls_api/serializers.py
from rest_framework import serializers
from rest_framework.validators import UniqueTogetherValidator
from .models import Vote
class VoteSerializer(serializers.ModelSerializer):
class Meta:
model = Vote
fields = ['question', 'choice', 'voter']
validators = [
UniqueTogetherValidator(
queryset=Vote.objects.all(),
fields=['question', 'voter']
)
]
문제 발생
- 500 에러 대신 400 에러가 발생하도록 변경되었지만, voter가 비어 있다는 오류가 발생했습니다.
- 이는 perform_create에서 is_valid()가 실행될 때 request.user가 data에 포함되지 않기 때문입니다.
해결 방법
- perform_create가 아닌 create 메서드를 오버라이드하여 voter를 강제 지정합니다.
# polls_api/views.py
from rest_framework import generics, status
from rest_framework.response import Response
from .models import Vote
from .serializers import VoteSerializer
class VoteCreateView(generics.CreateAPIView):
serializer_class = VoteSerializer
def create(self, request, *args, **kwargs):
new_data = request.data.copy()
new_data['voter'] = request.user.id # 현재 로그인한 사용자를 voter로 설정
serializer = self.get_serializer(data=new_data)
serializer.is_valid(raise_exception=True)
self.perform_create(serializer)
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
사용자가 임의로 voter를 변경하는 문제 방지
- 위의 코드 수정 후, 사용자가 PUT 요청을 통해 voter를 변경할 수 있는 보안 취약점이 발견.
- 이를 방지하기 위해 perform_update를 오버라이드
- 이제 사용자가 PUT 요청으로 voter 값을 변경해도 실제 저장되는 값은 현재 로그인한 사용자로 유지
# polls_api/views.py
from rest_framework import generics
from .models import Vote
from .serializers import VoteSerializer
class VoteDetailView(generics.RetrieveUpdateDestroyAPIView):
queryset = Vote.objects.all()
serializer_class = VoteSerializer
def perform_update(self, serializer):
serializer.save(voter=self.request.user) # voter를 요청한 사용자로 고정
Question에 속하지 않은 Choice에 대한 투표 방지
- 현재 구현에서는 특정 question과 관계없는 choice에 투표할 수 있는 문제가 있음
- 방지하기 위해 validate 메서드를 추가합
- 투표 시 question과 choice가 올바른 조합인지 검증할 수 있게 됨
# polls_api/serializers.py
class VoteSerializer(serializers.ModelSerializer):
class Meta:
model = Vote
fields = ['question', 'choice', 'voter']
def validate(self, attrs):
if attrs['choice'].question.id != attrs['question'].id:
raise serializers.ValidationError('선택한 Choice가 해당 Question에 속하지 않습니다.')
return attrs
Testing(테스팅)
Django에서 테스트를 작성하면 코드의 안정성을 높이고, 예기치 않은 버그를 방지할 수 있다. Django의 TestCase와 APITestCase를 활용하여 Serializer와 View의 테스트를 수행하는 방법을 설명
기본적인 테스트 구조
테스트 실행 방법
테스트 파일을 작성한 후 아래 명령어로 실행할 수 있습니다.
- test_로 시작하는 메서드만 자동으로 실행됩니다.
- 모든 테스트가 통과하면 .이 출력되고, 실패하면 F와 함께 오류 메시지가 표시됩니다.
python manage.py test
간단한 예제
polls_api/tests.py에서 간단한 테스트 코드 예시
- test_with_valid_data: 유효한 데이터를 넣었을 때 serializer가 정상 동작하는지 확인
- test_with_invalid_data: 잘못된 데이터를 넣었을 때 serializer가 실패하는지 확인
from django.test import TestCase
from polls_api.serializers import QuestionSerializer
class QuestionSerializerTestCase(TestCase):
def test_with_valid_data(self):
serializer = QuestionSerializer(data={'question_text': 'abc'})
self.assertTrue(serializer.is_valid())
new_question = serializer.save()
self.assertIsNotNone(new_question.id)
def test_with_invalid_data(self):
serializer = QuestionSerializer(data={'question_text': ''})
self.assertFalse(serializer.is_valid())
Serializer 테스트
VoteSerializer의 유효성 검사
- UniqueTogetherValidator: 같은 Question에 대해 한 사용자가 중복 투표하지 못하도록 설정
- 특정 Question에 속한 Choice만 선택할 수 있도록 제한
setUp 메서드를 활용한 코드 중복 제거
테스트 코드에서 반복적으로 객체를 생성하는 경우 setUp 메서드를 활용하면 중복을 줄일 수 있습니다.
from django.contrib.auth.models import User
from django.test import TestCase
from polls_api.models import Question, Choice
class VoteSerializerTestCase(TestCase):
def setUp(self):
self.user = User.objects.create_user(username='testuser', password='testpass')
self.question = Question.objects.create(question_text='Sample Question')
self.choice = Choice.objects.create(question=self.question, choice_text='Choice 1')
def test_vote_serializer(self):
self.assertEqual(User.objects.count(), 1) # 중복 생성 방지 확인
추가적인 테스트 케이스
def test_vote_serializer_with_unmatched_question_and_choice(self):
other_question = Question.objects.create(question_text='Another Question')
other_choice = Choice.objects.create(question=other_question, choice_text='Invalid Choice')
serializer = VoteSerializer(data={'question': self.question.id, 'choice': other_choice.id})
self.assertFalse(serializer.is_valid()) # 다른 질문에 속한 선택지는 허용되지 않아야 함
View 테스트
Serializer 테스트와 달리 View 테스트는 API 요청을 모방하여 진행합니다.
APITestCase 사용 이유
Django의 TestCase 대신 APITestCase를 사용하는 이유
- API 요청을 쉽게 테스트할 수 있음
- self.client.force_authenticate(user=user)를 사용하여 인증된 요청을 보낼 수 있음
테스트 코드 예제
from rest_framework.test import APITestCase
from django.urls import reverse
from django.contrib.auth.models import User
from polls_api.models import Question
class QuestionListTest(APITestCase):
def setUp(self):
self.user = User.objects.create_user(username='testuser', password='testpass')
self.client.force_authenticate(user=self.user)
self.question = Question.objects.create(question_text='Test Question')
def test_list_questions(self):
url = reverse('question-list')
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.data), Question.objects.count())
개별 테스트 실행
아래 명령어를 사용하면 특정 테스트 클래스만 실행할 수 있다.
python manage.py test polls_api.tests.QuestionListTest
테스트 검증 포인트
- 인증된 사용자가 Question을 생성할 수 있는지 확인
- 인증되지 않은 사용자가 Question을 생성하려고 할 때 실패하는지 확인
- GET 요청을 통해 Question 목록을 조회할 때 올바른 데이터가 반환되는지 검증
테스트 커버리지 측정
- 테스트가 얼마나 잘 작성되었는지 확인하려면 coverage 패키지를 사용
- 테스트 커버리지가 100%에 가까워질수록 테스트가 충분히 작성되었음을 의미
pip install coverage
coverage run manage.py test
coverage report'학습 주제 > Django & Django Rest Framework' 카테고리의 다른 글
| Django REST Framework - Votes (0) | 2025.03.10 |
|---|---|
| Django REST Framework - Postman & RelatedFiled (0) | 2025.03.10 |
| Django REST Framework - User 생성 & 사용자 권한 관리 (0) | 2025.03.10 |
| Django REST Framework - Views (0) | 2025.03.10 |
| Django REST Framework - Serializer & API (0) | 2025.03.10 |