Django REST Framework - Validation & Testing

2025. 3. 10.·학습 주제/Django & Django Rest Framework

출처: https://velog.io/@ohwani/Django-DRF-0-Django-Rest-Framework

 

 

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
'학습 주제/Django & Django Rest Framework' 카테고리의 다른 글
  • Django REST Framework - Votes
  • Django REST Framework - Postman & RelatedFiled
  • Django REST Framework - User 생성 & 사용자 권한 관리
  • Django REST Framework - Views
굥여9
굥여9
9idryd 님의 블로그 입니다.
  • 굥여9
    문과의 개발
    굥여9
  • 전체
    오늘
    어제
    • 분류 전체보기 (114)
      • 학습 주제 (86)
        • 자료구조와 알고리즘 (8)
        • HTML & 웹크롤링 (4)
        • 데이터 시각화 (4)
        • Django & Django Rest Framew.. (11)
        • AWS 클라우드 (6)
        • SQL & 데이터 웨어하우스 (11)
        • 데이터파이프라인과 Airflow (12)
        • Docker & K8S (8)
        • DBT (4)
        • CI & CD (1)
        • 빅데이터 처리와 Spark (12)
        • Kafka & Spark Streaming (5)
        • 보안 엔지니어링 (0)
      • 구름 프로펙트 클라우드 엔지니어링 (0)
        • [Monolithic] 서비스의 기초와 설계 (0)
        • [MSA & EDA] 비동기 전환과 정합성 (0)
        • [Cloud Native] K8s 기반 인프라와 .. (0)
      • 프로그래머스 데브코스 데이터 엔지니어링 (4)
      • 개발 기록 (24)
        • 일일 (24)
        • 주간 (0)
      • 회고 (0)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

    • 깃허브
  • 공지사항

  • 인기 글

  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
굥여9
Django REST Framework - Validation & Testing
상단으로

티스토리툴바