본문 바로가기

데브코스/TIL

[TIL] 4주차_Day15: Django 프레임워크를 사용해서 API서버 만들기(5)

💡Today I Learned

  • 파이썬 장고 프레임워크를 이용한 API 서버 만들기의 다섯 번째 강의를 진행했습니다.
  • RelatedField, 투표 기능 추가(Model, Serialier, View 구현), Validation에 대한 이론 및 실습

 


 

1. RelatedField

a. StringRelatedField: 모델의 __str__(self) 에 지정된 내용을 questions 필드에 표시

class UserSerializer(serializers.ModelSerializer):
	questions = serializers.StringRelatedField(many=True, read_only=True)

 

b. SlugRelatedField: 지정한 slug_field 내용을 questions 필드에 표시

class UserSerializer(serializers.ModelSerializer):
	questions = serializers.SlugRelatedField(many=True, read_only=True, slug_field='pub_date')

 

c. HyperlinkedRelatedField: questions 필드에 해당 질문의 내용을 볼 수 있는 하이퍼링크를 표시

class UserSerializer(serializers.ModelSerializer):
	questions = serializers.HyperlinkedRelatedField(many=True, read_only=True, view_name='question-detail')

: 뷰 지정 → 'question-detail' 이름의 'question/<int:pk>/' url path로 이동하기

 

d. Question 하나에서 연관된 Choice set 확인하기: Choice 모델에서 related_name=choices로 변경 → Question에서 사용 시 choice_set 대신 choices 가능

class ChoiceSerializer(serializers.ModelSerializer):
	class Meta:
		model = Choice
		fields = ['choice_text', 'votes']

 

class QuestionSerializer(serializers.ModelSerializer):
	# ...
	choices = ChoiceSerializer(many=True, read_only=True) # related_name=choices로 변경, choice_set -> choices

	class Meta:
		model = Question
		fields = ['id', 'question_text', 'pub_date', 'owner', 'choices']

 

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

 

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

: 사용자 단위로 votes 관리 = 로그인한 사용자만 투표하도록 함 

 

a. Vote 모델 생성

from django.contrib.auth.models import User

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)

 

b. Unique 제약조건 명시: 하나의 question & 하나의 voter로 투표 가능 → 특정 question과 voter에 대해서는 하나의 레코드만 생성됨 (동일 voter로는 하나의 question에 대해서 하나의 vote만 가능)

class Vote(models.Model):
	# ...

	class Meta:
		constraints = [
			models.UniqueConstraint(fields=['question', 'voter'], name='uniques_voter_for_questions')
		]

 

c. votes 개수 표시하기 → Choice의 votes필드(IntegerField라서 vote 후에도 페이지에서 표시가 안됨?) 대신 ChoiceSerializer 안에서 votes_set 필드(SerializerMethodField) 만들어 choice.vote_set을 카운팅해주기

class ChoiceSerializer(serializers.ModelSerializer):
	votes_count = serializers.SerializerMethodField() # MethodField: 값이 메소드에 의해 지정됨

	class Meta:
		model = Choice
		# fields = ['choice_text', 'votes']
		fields = ['choice_text', 'votes_count']

	def get_votes_count(self, obj): # obj = Choice, 특정 Choice를 참조하는 Vote obj의 개수 counting
		return obj.vote_set.count() # related_name 설정 안했으므로 vote_set

 

3. 투표 기능 구현(2) - Serializer, View

: 3-(1) 장고 쉘에서 Vote 만들었음

: 3-(2) 직접 Vote를 만들 수 있는 serializer, view 구성

 

a. VoteSerializer (serializers.py)

class VoteSerializer(serializers.ModelSerializer):
	voter = serializers.ReadOnlyField(source='voter.username')

	class Meta:
		model = Vote
		fields = ['id', 'question', 'choice', 'voter']

 

b. VoteList, VoteDetail (views.py) + voter만 자신의 Vote에 대해서 get, put, post 요청 가능 → permissions 지정 (permissions.py)

class VoteList(generics.ListCreateAPIView):
	serializer_class = VoteSerializer
	permission_classes = [permissions.IsAuthenticated] # 로그인x인 경우 조회도 불가

	# 로그인한 사용자(request.user)의 vote만 조회 가능
	def get_queryset(self, *args, **kwargs):
		return Vote.objects.filter(voter=self.request.user)

	# vote 등록(create) 시 이미 로그인 한 user(=self.request.user)을 voter로 지정하라
	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.IsAuthenticatedOrReadOnly, IsVoter] # 작성자만 get, put, post 가능

 

# 로그인한 사용자만, 나머지는 모든 권한 x
class IsVoter(permissions.BasePermission):
	def has_object_permission(self, request, view, obj):
		return obj.voter == request.user

 

4. Validation

a. 에러 코드 수정하기 (동일 voter로 한 question에 두 개의 vote 시도할 때 5xx대 에러 → 4xx대 에러로) → UniquesTogetherValidator

class VoteSerializer(serializers.ModelSerializer):
	# ...
	class Meta:
	# ...
	# VoteList 뷰의 (타고타고 들어가서) serializer.is_valid()가 실행될 때 validation을 수행하는 방법을 시리얼라이저에서 정의
		validators = [
			UniqueTogetherValidator(
				queryset=Vote.objects.all(),
				fields=['question', 'voter'] # question, voter가 유니크한지 확인
			)
		]

 

b. is_valid() 호출 시 voter=request.user 로 voter를 지정해줘야 함 (.perform_create() 보다 이전 단계에서 is_valid()가 호출되므로 .perform_create() 함수 오버라이딩 해서 voter 지정해주는 것은 의미 x)

class VoteList(generics.ListCreateAPIView):
	# ...
	# vote 등록(create_오버라이딩)시 이미 로그인 한 user(=request.user.id)을 voter로 지정하라
	def create(self, request, *args, **kwargs):
		new_data = request.data.copy()
		new_data['voter'] = request.user.id # 현재 로그인 한 user.id를 'voter'로

		serializer = self.get_serializer(data=new_data)
		serializer.is_valid(raise_exception=True)
		self.perform_create(serializer) # voter의 Vote 생성
		headers = self.get_success_headers(serializer.data)
		return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)

 

 

c. Question에 맞는 Choice만 선택지로 제공하기 (맞지 않으면 400 Bad Request error raise)

class VoteSerializer(serializers.ModelSerializer):
	# ...
	def validate(self, attrs):
		if attrs['choice'].question.id != attrs['question'].id:
			raise serializers.ValidationError("Question과 Choice가 조합이 맞지 않습니다.")

		return attrs

 

5. Testing

a. TestCase 상속받은 QuestionSerializerTestCase 클래스 생성 (tests.py)

from django.test import TestCase

# Create your tests here.
class QuestionSerializerTestCase(TestCase):
    def test_a(self): # 'test_' 로 시작하는 메소드만 테스트라고 판단함
	    pass

    def test_b(self):
    	pass

    def some_method(self): # 실행 x
    	print("This is test some method")

 

python manage.py test (shell)

: mysite 내의 모든 TestCase를 수행함

: 터미널 상에서 '.' 출력 = 문제없이 동작했다는 뜻

 

b. A, B가 같은지 확인 (self.assertEqual(A, B))

class QuestionSerializerTestCase(TestCase):
    def test_a(self): 
	    self.assertEqual(1, 2)

: 터미널 상에서 'F'출력 = failed

 

def test_with_valid_data(self):
    serializer = QuestionSerializer(data={'question_text': 'abc'})
    self.assertEqual(serializer.is_valid(), True)

def test_with_invalid_data(self):
    serializer = QuestionSerializer(data={'question_text': ''})
    self.assertEqual(serializer.is_valid(), False)

 

c. A가 None인지 확인 (self.assertIsNotNone(A))

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)

 

6. Testing serializers

a. VoteSerializer 테스트: test_vote_serializer (제대로 만든 vote에 대한 테스팅)

class VoteSerializerTestCase(TestCase):
    def test_vote_serializer(self):
        user = User.objects.create(username='testuser')
        question = Question.objects.create(
            question_text = 'abc',
            owner=user,
        )
        choice = Choice.objects.create(
            question=question,
            choice_text='1',
        )
        data = {
            'question': question.id,
            'choice': choice.id,
            'voter': user.id
        }
        serializer = VoteSerializer(data=data)
        self.assertTrue(serializer.is_valid())
        vote = serializer.save()

        self.assertEqual(vote.question, question)
        self.assertEqual(vote.choice, choice)
        self.assertEqual(vote.voter, user)

 

b. VoteSerializer 테스트: test_vote_serializer_with_duplicate_vote (중복된 vote에 대한 테스팅)

class VoteSerializerTestCase(TestCase):
    # ...
    def test_vote_serializer_with_duplicate_vote(self):
        user = User.objects.create(username='testuser')
        question = Question.objects.create(
            question_text = 'abc',
            owner=user,
        )
        choice = Choice.objects.create(
            question=question,
            choice_text='1',
        )
        choice1 = Choice.objects.create(
            question=question,
            choice_text='2',
        )

        # 이미 question에 대해 voter가 'choice'로 투표한 상항
        Vote.objects.create(question=question, choice=choice, voter=user)

        # 동일 voter가 question에 대해 또다른 옵션 'choice1'으로 재투표
        data = {
            'question': question.id,
            'choice': choice1.id, # choice가 아닌 choice1로!
            'voter': user.id
        }

        # .is_valid()가 False이면 assertFalse 함수가 True로 리턴, 이 테스트는 성공
        # .is_valid() 내에서 중복 투표인지 확인하니까 해당 검증이 이뤄졌다는 의미
        serializer = VoteSerializer(data=data)
        self.assertFalse(serializer.is_valid())

 

c. .setUp() 오버라이딩: 각 테스트(test_) 실행될 때마다 반복돼서 실행됨 (test_~ 함수에서 반복적으로 실행되는 내용)

class VoteSerializerTestCase(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',
        )

: 이후 멤버변수(self.user, self.question, self.choice)는 test_ 함수에서 멤버변수로 사용하기 (self.~)

*) Testing은 테스트용 별도 DB를 사용함

*) setUp으로 만든 내용은 다음 테스트에도 남아있는 게 아님 (테스트마다 독립적으로 사용됨, 하나의 test_ 수행 후 사라지고, 그 다음 test_ 수행 시 만들어짐) → 다음 테스트 수행 전 초기화

 

d. VoteSerializer 테스트: test_vote_serializer_with_unmatched_question_and_choice (Question에 대해 다른 Choice 선택 시 False인지 확인)

class VoteSerializerTestCase(TestCase):
#...
	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, # 다른 question에 대한 choice -> assertFalse()는 True
            'voter': self.user.id
        }
        serializer = VoteSerializer(data=data)
        self.assertFalse(serializer.is_valid())

 

7. Testing Views

a. APITestCase 상속(for View 테스트, 제공하는 함수 이용) 받은 QuestionListTest 클래스 구현

class QuestionListTest(APITestCase):
    def setUp(self):
        self.question_data = {'question_text': 'some question'}
        self.url = reverse('question-list') # method 안에서 사용할 때는 reverse_lazy 아닌 reverse! ('name'에 대한 url 만들어주는 같은 역할)

    def test_create_question(self):
        user = User.objects.create(username='testuser', password='testpass')
        self.client.force_authenticate(user=user) # 사용자를 강제로 로그인, 앞으로 로그인 된 상태임
        reponse = self.client.post(self.url, self.question_data) # url에 question_data로 post요청 보내기
        self.assertEqual(reponse.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) # A보다 B가 작으면 True / 방금 만들어진 질문인지 학인

    def test_create_question_without_authentication(self): # 로그인 없이 질문 만드려고 할 때 403 에러 발생해야 됨
        reponse = self.client.post(self.url, self.question_data)
        self.assertEqual(reponse.status_code, status.HTTP_403_FORBIDDEN)

    def test_list_questions(self): # question 생성 시 question list에 잘 반영되는지 확인
        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)

 

python manage.py test polls_api.tests.QuestionListTest

: [앱 이름].tests.[테스트케이스 클래스] 특정 지어서 하나만 테스팅 가능

 

b. coverage: 코드가 얼마나 잘 테스트 되고있는지 확인 (%로 출력됨)

pip install coverage

coverage run manage.py test

coverage report
반응형