💡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
'데브코스 > TIL' 카테고리의 다른 글
[TIL] 5주차_Day17: 크롤한 웹데이터로 만들어보는 웹사이트 (2) (0) | 2023.11.07 |
---|---|
[TIL] 5주차_Day16: 크롤한 웹데이터로 만들어보는 웹사이트 (1) (0) | 2023.11.06 |
[TIL] 4주차_Day14: Django 프레임워크를 사용해서 API서버 만들기(4) (0) | 2023.11.06 |
[TIL] 4주차_Day13: Django 프레임워크를 사용해서 API서버 만들기(3) (1) | 2023.11.04 |
[TIL] 4주차_Day12: Django 프레임워크를 사용해서 API서버 만들기(2) (1) | 2023.11.03 |