[TIL] 파이썬 장고 프레임웍을 사용해서 API 서버 만들기 (5)

이원진·2023년 4월 28일
0

데브코스

목록 보기
15/54
post-thumbnail
post-custom-banner

학습내용


  1. RelatedField

  2. 투표(Votes) 기능 구현하기 1 - Models

  3. 투표(Votes) 기능 구현하기 2 - Serializers & Views

  4. Validation

  5. Testing

  6. Testing Serializers

  7. Testing Views

1. RelatedField


  • PrimaryKeyRelatedField: 모델의 Primary Key 사용

  • StringRelatedField: 모델의 __str__() 메서드 사용

  • SlugRelatedField: 사용할 모델의 필드 지정 가능

    # "pub_date"라는 이름의 필드 사용
    q = serializers.SlugRelatedField(..., slug_field = "pub_date")

  • HyperlinkedRelatedField: View 이름을 사용해 링크 기능 제공

    q = serializers.HyperlinkedRelatedField(..., view_name = "question-detail")

  • Question 객체에서 Choice 객체의 집합(choice_set) 볼 수 있는 기능 구현

    • polls/models.py

      class Choice(models.Model):
          question = models.ForeignKey(Question, related_name = "choices", on_delete = models.CASCADE)
              ...

    • polls_api/serializers.py

      class ChoiceSerializer(serializers.ModelSerializer):
          class Meta:
              model = Choice
              fields = ["choice_text", "votes"]
      
      class QuestionSerializer(serializers.ModelSerializer):
              ...
          choices = ChoiceSerializer(many = True, read_only = True)
      
          class Meta:
              model = Question
              fields = ["id", "question_text", "pub_date", "owner", "choices"]
      
      class UserSerializer(serializers.ModelSerializer):
              # User의 Question 객체를 HyperLinkedKeyRelatedField로 연결해 링크 제공
          questions = serializers.HyperlinkedRelatedField(many = True, read_only = True, view_name = "question-detail")
      
          ...

2. 투표(votes) 기능 구현하기 1 - Models


  • serializers.SerializerMethodField(): 정의한 메서드의 반환값 사용

  • polls/models.py

    class Vote(models.Model):
        question = models.ForeignKey(Question, on_delete = models.CASCADE)
        choice = models.ForeignKey(Choice, on_delete = models.CASCADE)
        voter = models.ForeignKey(User, on_delete = models.CASCADE)
    
        class Meta:
            # 각 사용자가 하나의 질문에 한 번만 투표할 수 있도록 UNIQUE 제약조건 생성
            constraints = [
                models.UniqueConstraint(fields = ["question", "voter"], name = "unique_voter_for_questions")
            ]

  • polls_api/serializers.py

    from rest_framework import serializers
    from polls.models import Question, Choice
    ...
    
    class ChoiceSerializer(serializers.ModelSerializer):
        votes_count = serializers.SerializerMethodField()
    
        class Meta:
            model = Choice
            fields = ["choice_text", "votes_count"]
    
        def get_votes_count(self, obj):
            # 각 선택지의 투표 개수
            return obj.vote_set.count()

3. 투표(votes) 기능 구현하기 1 - Serializers & Views


  • polls_api/serializers.py

    ...
    
    class VoteSerializer(serializers.ModelSerializer):
        voter = serializers.ReadOnlyField(source = "voter.username")
    
        class Meta:
            model = Vote
            fields = ["id", "question", "choice", "voter"]
    ...

  • polls_api/urls.py

    ...
    
    urlpatterns = [
        ...
        path('vote/', VoteList.as_view()),
        path('vote/<int:pk>/', VoteDetail.as_view()),
    ]

  • polls_api/views.py

    ...
    from .permission import IsOwnerOrReadOnly, IsVoter
    
    class VoteList(generics.ListCreateAPIView):
        serializer_class = VoteSerializer
        permission_classes = [permissions.IsAuthenticated]
    
        # 내가 작성한 Vote만 목록으로 출력
        def get_queryset(self, *args, **kwargs):
            return Vote.objects.filter(voter = self.request.user)
    
        # 투표 시 투표자 저장
        def perform_create(self, serializer):
            serializer.save(voter = self.request.user)
    
    class VoteDetail(generics.RetrieveUpdateDestroyAPIView):
        queryset = Vote.objects.all()
        serializer_class = VoteSerializer
        permission_classes = [permissions.IsAuthenticated, IsVoter]
    
    ...

  • polls_api/permission.py

    ...
    
    # 투표자가 아니라면, 수정과 읽는 요청 모두 불가능
    class IsVoter(permissions.BasePermission):
        def has_object_permission(self, request, view, obj):
            return obj.voter == request.user

4. Validation


  • 사용자가 올바르게 검증했는지 검증

  • polls_api/serializers.py

    ...
    from rest_framework.validators import UniqueTogetherValidator
    
    class VoteSerializer(serializers.ModelSerializer):
        # 질문에 대해 유효한 답변을 선택했는지 검증
        def validate(self, attrs):
            if attrs["choice"].question.id != attrs["question"].id:
                raise serializers.ValidationError("Question과 Choice의 조합이 맞지 않습니다.")
    
            return attrs
    
        class Meta:
            model = Vote
            fields = ["id", "question", "choice", "voter"]
            
            # 각 사용자가 하나의 질문에 하나의 답변만 했는지 검증
            validators = [
                UniqueTogetherValidator(
                queryset = Vote.objects.all(),
                fields = ["question", "voter"]
                )
            ]
    
    ...

  • polls_api/views.py

    ...
    
    class VoteList(generics.ListCreateAPIView):
        ...
    
        # 유효성 검사 하기 전에 voter를 설정하기 위해 perform_create()가 아닌 create()를 재정의
        def create(self, request, *args, **kwargs):
            new_data = request.data.copy()
            new_data["voter"] = request.user.id
            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)
    
    class VoteDetail(generics.RetrieveUpdateDestroyAPIView):
        ...
    
        def perform_update(self, serializer):
            serializer.save(voter = self.request.user)
    
    ...

5. Testing


  • 테스트는 개발 시간을 줄이고, 문제를 예방하거나 발견할 수 있으며, 협업하기 좋은 코드를 만들 수 있다는 장점이 있음

  • python manage.py test

    • tests.py의 'test_' 로 시작하는 테스트 메서드를 실행

  • polls_api/tests.py

    from django.test import TestCase
    from polls_api.serializers import QuestionSerializer
    
    # Create your tests here.
    class QuestionSerializerTestCase(TestCase):
        # QuestionSerializer의 is_valid() 메서드와 save() 메서드가 제대로 동작하는지 테스트
        def test_with_valid_data(self):
            serializer = QuestionSerializer(data = {"question_text": "abc"})
            self.assertEqual(serializer.is_valid(), True)
    
            new_question = serializer.save()
            self.assertIsNotNone(new_question.id)
    
        def test_with_invalid_test(self):
            serializer = QuestionSerializer(data = {"question_text": ""})
            self.assertEqual(serializer.is_valid(), False)

6. Testing Serializers


  • 테스트 메서드의 이름은 테스트 내용을 파악할 수 있도록 짓는 것이 좋음

  • 테스트 실행 시, 테스트를 위한 임시 데이터베이스를 생성해서 테스트한 뒤 삭제

  • polls_api/tests.py

    ...
    from polls_api.serializers import QuestionSerializer, VoteSerializer
    from polls.models import Question, Choice, Vote
    
    class VoteSerializerTest(TestCase):
    
        # 각 테스트 메서드가 실행되기 전에 실행되는 설정 메서드
        # 각 테스트 메서드마다 새로운 데이터 생성
        def setUp(self):
            self.user = User.objects.create(username = "testUser")
    
            self.question = Question.objects.create(
                question_text = "abc",
                owner = self.user
            )
    
            self.choice = Choice.objects.create(
                question = self.question,
                choice_text = "1"
            )
    
        # 새로운 Vote 객체가 잘 생성되는지 테스트
        def test_vote_serializer(self):
            data = {
                "question": self.question.id,
                "choice": self.choice.id,
                "voter": self.user.id
            }
    
            serializer = VoteSerializer(data = data)
            self.assertTrue(serializer.is_valid())
    
            vote = serializer.save()
            self.assertEqual(vote.question, self.question)
            self.assertEqual(vote.choice, self.choice)
            self.assertEqual(vote.voter, self.user)
    
        # User가 이미 투표한 상태에서 또 투표하려고 할 때 ValidationError가 잘 발생하는지 테스트
        def test_vote_serializer_with_duplicate_vote(self):
            choice1 = Choice.objects.create(
                question = self.question,
                choice_text = "2"
            )
    
            Vote.objects.create(question = self.question, choice = self.choice, voter = self.user)
    
            data = {
                "question": self.question.id,
                "choice": self.choice.id,
                "voter": self.user.id
            }
    
            serializer = VoteSerializer(data = data)
            self.assertFalse(serializer.is_valid())
    
        # Question과 Choice의 조합이 맞지 않을 경우 ValidationError가 잘 발생하는지 테스트
        def test_vote_serializer_with_unmatched_question_and_choice(self):
            question2 = Question.objects.create(
                question_text = "abc",
                owner = self.user
            )
    
            choice2 = Choice.objects.create(
                question = question2,
                choice_text = "1"
            )
    
            data = {
                "question": self.question.id,
                "choice": choice2.id,
                "voter": self.user.id
            }
    
            serializer = VoteSerializer(data = data)
            self.assertFalse(serializer.is_valid())

7. Testing Views


  • Serializer를 테스트할 때는 Django의 TestCase 라이브러리를 사용하지만, View를 테스트할 때는 DRF의 APITestCase 라이브러리 사용

  • reverse()reverse_lazy()와 같은 역할을 하며, 메서드 내에서 사용

  • python manage.py test app_name.tests.test_name: 특정 테스트 메서드만 실행

  • polls_api/tests.py

...
from django.urls import reverse
from django.utils import timezone
from rest_framework import status
from rest_framework.test import APITestCase

class QuestionListTest(APITestCase):
    def setUp(self):
        self.question_data = {"question_text": "some question"}
        self.url = reverse("question-list")

    # Question을 생성하는 요청을 보냈을 때 잘 처리되는지 테스트
    def test_create_question(self):
        user = User.objects.create(username = "testuser", password = "testpass")

        # 강제 인증
        self.client.force_authenticate(user = user)
        response = self.client.post(self.url, self.question_data)

        self.assertEqual(response.status_code, status.HTTP_201_CREATED)
        self.assertEqual(Question.objects.count(), 1)

        question = Question.objects.first()
        self.assertEqual(question.question_text, self.question_data["question_text"])
        self.assertLess((timezone.now() - question.pub_date).total_seconds(), 1)

    # 인증되지 않은 사용자가 POST 요청을 보낼 경우 403에러가 잘 발생하는지 테스트
    def test_create_question_without_authentication(self):
        response = self.client.post(self.url, self.question_data)
        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)

    # 테스트 목록을 요청했을 때 잘 처리되는지 테스트
    def test_list_question(self):
        question = Question.objects.create(question_text = "Question1")
        choice = Choice.objects.create(question = question, choice_text = "choice1")
        Question.objects.create(question_text = "Question2")
        response = self.client.get(self.url)

        self.assertEqual(response.status_code, status.HTTP_200_OK)
        self.assertEqual(len(response.data), 2)
        self.assertEqual(response.data[0]["choices"][0]["choice_text"], choice.choice_text)

메모



post-custom-banner

0개의 댓글