JUST DO IT!

[TIL]KDT_20230428 ๋ณธ๋ฌธ

TIL

[TIL]KDT_20230428

sunhokimDev 2023. 4. 28. 17:07

๐Ÿ“š KDT WEEK 4 DAY 5 TIL

  • RelatedField
  • ํˆฌํ‘œ(Votes)๊ธฐ๋Šฅ ๊ตฌํ˜„ํ•˜๊ธฐ
  • Validation
  • Testing

 


 

๐ŸŸฅ RelatedField

Serializer์—์„œ ์™ธ๋ž˜ํ‚ค๋กœ ์—ฐ๊ฒฐ๋œ ๋‹ค๋ฅธ ํ•„๋“œ๋ฅผ ๋ถˆ๋Ÿฌ์˜ฌ ๋•Œ, ์–ด๋–ค ์˜ต์…˜์œผ๋กœ ๊ฐ€์ ธ์˜ฌ์ง€ ์ด ๊ธฐ๋Šฅ์œผ๋กœ ์ •ํ•  ์ˆ˜ ์žˆ๋‹ค.

 

1. PrimaryKeyRealatedField

questions = serializers.PrimaryKeyRelatedField(many=True, queryset=Question.objects.all())

PrimaryKey, ์ฆ‰ id์˜ ๊ฐ’์„ ๋ณด์—ฌ์ค€๋‹ค.

 

2. StringRelatedField

questions = serializers.StringRelatedField(many=True, read_only=True) # __str__ ์ฐธ์กฐ๋จ

ํ•ด๋‹น ๋ชจ๋ธ(questions)์— ์ •์˜๋œ __str__์— ๋”ฐ๋ผ ์ด๋ฆ„์œผ๋กœ ๋ณด์—ฌ์ค€๋‹ค.

 

3. SlugRelatedField

questions = serializers.SlugRelatedField(many=True, read_only=True, slug_field="pub_date") # ํŠน์ • ํ•„๋“œ๊ฐ€์ ธ์˜ด

ํ•ด๋‹น ์˜ค๋ธŒ์ ํŠธ(question)์˜ ํŠน์ • ํ•„๋“œ ๊ฐ’(pub_date)๋ฅผ ๋ณด์—ฌ์ค€๋‹ค.

 

4. HyperlinkedRelatedField

questions = serializers.HyperlinkedRelatedField(many=True, read_only=True, view_name='question-detail'

ulrs.py์˜ name๊ณผ ์ผ์น˜ํ•˜๋Š” view_name์„ ์ž‘์„ฑํ•˜๋ฉด, ํ•ด๋‹น ์˜ค๋ธŒ์ ํŠธ(question)๊ณผ ์—ฐ๊ฒฐ๋˜๋Š” url์„ ๋ณด์—ฌ์ค€๋‹ค.

 

๊ฒฐ๊ณผ

HyperlinkedRealtedFiled ๋กœ url์„ ๋‚˜ํƒ€๋‚ธ ๋ชจ์Šต

 


 

์ด๋ฒˆ์—” Serializer๋ฅผ ์ •์˜ํ•˜์—ฌ ๊ด€๋ จ๋ชจ๋ธ ํ‘œ๊ธฐํ•ด๋ณด๊ธฐ

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

class QuestionSerializer(serializers.ModelSerializer):
    owner = serializers.ReadOnlyField(source='owner.username')
    choices = ChoiceSerializer(many=True, read_only = True) # Question์—์„œ choice๋ฅผ ํ‘œ์‹œํ•˜๊ธฐ ์œ„ํ•ด์„œ

    class Meta:
        model = Question
        fields = ['id', 'question_text', 'pub_date','owner', 'choices'] # ModelSerializer๋ฅผ ํ†ตํ•ด ์ž๋™์œผ๋กœ ์ƒ์„ฑ

Question์— ์—ฐ๊ฒฐ๋œ choice๋“ค์„ ๋‚˜ํƒ€๋‚ด๊ธฐ ์œ„ํ•ด์„œ ChoiceSerializer๋ฅผ ๋งŒ๋“ค๊ณ , ์œ„์ฒ˜๋Ÿผ ์ž‘์„ฑํ•œ๋‹ค.

choices = ChoiceSerializer๊ณผ fields์— 'choices'๋ฅผ ์ถ”๊ฐ€ํ•ด์ฃผ์—ˆ๋‹ค.

 

+

์‚ฌ์‹ค choices๋กœ ์ด๋ฆ„์„ ์ •ํ•œ ๊ฒƒ์€, Choice ๋ชจ๋ธ์—์„œ ์ •์˜ํ•œ ๋ถ€๋ถ„๊ณผ ๊ด€๋ จ์ด ์žˆ๋‹ค.

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

Chocie ๋ชจ๋ธ์•ˆ์—์„œ question์— related_name์„ 'choices'๋กœ ์ •์˜ํ–ˆ๊ธฐ ๋•Œ๋ฌธ์— ๊ฐ€๋Šฅํ•œ ์ผ์ด๋‹ค.

์œ„์ฒ˜๋Ÿผ related_name์˜ ์ •์˜๊ฐ€ ์—†๋‹ค๋ฉด, shell์—์„œ ๋™์ž‘ํ•˜๋“ฏ choice_set์œผ๋กœ ๋ถˆ๋Ÿฌ์™€์•ผํ•œ๋‹ค.

 

 


 

๐ŸŸฆ ํˆฌํ‘œ ๊ธฐ๋Šฅ ๊ตฌํ˜„ํ•˜๊ธฐ

๊ธฐ๋Šฅ ๊ตฌํ˜„์„ ์œ„ํ•ด ๋ˆ„๊ฐ€ ์–ด๋–ค ์งˆ๋ฌธ์— ์–ด๋–ค ๊ฒƒ์„ ํˆฌํ‘œํ–ˆ๋Š”์ง€๋ฅผ ๋‹ด๊ณ  ์žˆ๋Š” ๊ฐ์ฒด, Vote ๋ชจ๋ธ์„ ๋งŒ๋“ค์–ด๋ณด์ž.

 

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)

    # question๊ณผ voter์— ๋Œ€ํ•ด ํ•˜๋‚˜์˜ ๋ ˆ์ฝ”๋“œ๋ฅผ ์ƒ์„ฑ
    # voter๊ฐ€ ํ•˜๋‚˜์”ฉ๋งŒ ๋งŒ๋“ค ์ˆ˜ ์žˆ๊ฒŒ ์ œ์•ฝ์กฐ๊ฑด์„ ๋งŒ๋“ค์–ด์ฃผ๋Š” ๊ฒƒ์ž„.
    class Meta:
        constraints = [
            models.UniqueConstraint(fields=['question', 'voter'], name='unique_voter_for_questions')
        ]

 

ํ•˜๋‚˜์˜ User๊ฐ€ ๊ฐ™์€ Question์— ์—ฌ๋Ÿฌ๋ฒˆ ํˆฌํ‘œํ•˜๋Š” ๊ฒƒ์„ ๋ง‰๊ธฐ ์œ„ํ•ด, ์ œ์•ฝ์กฐ๊ฑด(Constratins)๋ฅผ ์ž‘์„ฑํ•ด์ฃผ์—ˆ๋‹ค.

์ œ์•ฝ์กฐ๊ฑด์€ ์œ„์ฒ˜๋Ÿผ, Meta ํด๋ž˜์Šค์•ˆ์— ๋ฆฌ์ŠคํŠธํ˜•ํƒœ๋กœ ๊ตฌํ˜„ํ•œ๋‹ค.

์ด๋ ‡๊ฒŒ question๊ณผ voter๋ฅผ ๋ฌถ์–ด๋†“์œผ๋ฉด, ํ•˜๋‚˜์˜ User๊ฐ€ Choice๋งŒ ๋ฐ”๊ฟ”์„œ ๊ฐ™์€ Question์— ํˆฌํ‘œํ•  ์ˆ˜ ์—†๊ฒŒ๋  ๊ฒƒ์ด๋‹ค!

 

์ด์ œ Choice์—์„œ๋Š” ์ž์‹ ์˜ voteํ•„๋“œ๊ฐ€ ์•„๋‹Œ Vote ๋ชจ๋ธ์„ ํ†ตํ•ด ํˆฌํ‘œ์ˆ˜๋ฅผ ๋ณด์—ฌ์ค˜์•ผํ•  ๊ฒƒ์ด๋‹ค.

 

class ChoiceSerializer(serializers.ModelSerializer):
    votes_count = serializers.SerializerMethodField() # ๊ฐ’์ด ๋ฉ”์†Œ๋“œ์— ์˜ํ•ด์„œ ๊ฒฐ์ •๋จ

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

    def get_votes_count(self, obj): # obj = choice
        return obj.vote_set.count() # choice๊ฐ€ vote์™€ ์™ธ๋ž˜ํ‚ค๋กœ ์—ฐ๊ฒฐ๋˜์–ด์žˆ๊ธฐ ๋•Œ๋ฌธ์— _set ์‚ฌ์šฉ

serializers.SerializerMethodField()๋Š” ์ด ๋ณ€์ˆ˜๊ฐ€ ํ•จ์ˆ˜์˜ ๊ฐ’์— ์˜ํ•ด ๊ฒฐ์ •๋œ๋‹ค๊ณ  ๋งํ•ด์ฃผ๋Š” ๊ฒƒ์ด๋‹ค.

 

์•„๋ž˜์— get_๋ณ€์ˆ˜์ด๋ฆ„()์˜ ๋ฉ”์†Œ๋“œ์—์„œ return ๊ฐ’์„ ํ†ตํ•ด votes_count์— ์ €์žฅ๋  ๊ฒƒ์ด๋‹ค!

 

ํƒœ์ŠคํŠธ๋ฅผ ์œ„ํ•ด Shell์—์„œ user,question,choice๋ฅผ ์ž„์˜๋กœ ํ•˜๋‚˜ ์ €์žฅํ•ด๋‘๊ณ  ์•„๋ž˜์™€ ๊ฐ™์ด ์ž…๋ ฅํ–ˆ๋‹ค.

>>> Vote.objects.create(voter=user,question=question,choice=choice)
<Vote: Vote object (1)>

 

๊ฒฐ๊ณผ

shell์—์„œ ๋™์ž‘ํ•œ ๊ฒฐ๊ณผ๊ฐ€ ์ž˜ ๋‚˜ํƒ€๋‚˜๊ณ  ์žˆ๋‹ค.

 


 

Vote ์ƒ์„ฑ ๋ฐ ์ˆ˜์ • API ๋งŒ๋“ค๊ธฐ

API ์ƒ์„ฑ์„ ์œ„ํ•ด ๋จผ์ € Serializer๋ฅผ ์ƒ์„ฑํ•ด์•ผํ•œ๋‹ค.

 

โ–ถ polls_api/serializers.py

from polls.models import Question,Choice, Vote

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

voter๋Š” ๋กœ๊ทธ์ธ ์‚ฌ์šฉ์ž๋ฅผ ํ† ๋Œ€๋กœ ์ •์˜๋˜๊ฑฐ๋‚˜ ์ˆ˜์ •๋˜๋ฏ€๋กœ ReadonlyField๋ฅผ ๋„ฃ์–ด์ฃผ์—ˆ๋‹ค.

 

๋‹ค์Œ์œผ๋กœ ํ™”๋ฉด ๊ตฌํ˜„์„ ์œ„ํ•ด views.py์— Question์ฒ˜๋Ÿผ VoteList์™€ VoteDetail์„ ๋งŒ๋“ค์—ˆ๋‹ค.

 

โ–ถ polls_api/views.py

#๋‚ด๊ฐ€ ์ž‘์„ฑํ•œ Vote๋“ค๋งŒ ๋ณด์—ฌ์ฃผ๋Š” ๊ธฐ๋Šฅ
class VoteList(generics.ListCreateAPIView):
    serializer_class = VoteSerializer
    permission_classes = [permissions.IsAuthenticated] # ๋กœ๊ทธ์ธํ•œ ์œ ์ €๋งŒ ๋ณผ ์ˆ˜ ์žˆ๋„๋ก ์ œํ•œ

    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] #ํˆฌํ‘œํ•œ ์‚ฌ๋žŒ๋งŒ CRUD ๊ฐ€๋Šฅํ•ด์•ผํ•จ

VoteList๋Š” vote๋“ค์˜ ๋ชฉ๋ก์„ ๋ณด์—ฌ์ค„ ๊ฒƒ์ด๋‹ค.

vote๊ฐ€ ์ƒ์„ฑ๋  ๋•Œ๋Š” ํ˜„์žฌ ๋กœ๊ทธ์ธํ•œ ์‚ฌ์šฉ์ž์˜ ๋ฐ์ดํ„ฐ๋ฅผ ๋„ฃ์–ด์•ผ ํ•˜๋ฏ€๋กœ perform_create๋ฅผ ์˜ค๋ฒ„๋ผ์ด๋”ฉํ•ด์•ผํ•œ๋‹ค.

 

VoteDetail๋Š” vote id๋ฅผ ํ†ตํ•ด ๋งŒ๋“ค์–ด์ง„ ์ƒ์„ธํŽ˜์ด์ง€์ด๋‹ค.

์—ฌ๊ธฐ์„œ๋Š” permission์— IsVoter๋ผ๋Š” ํŠน๋ณ„ํ•œ ํ•จ์ˆ˜๋ฅผ ๋„ฃ์–ด์ฃผ์—ˆ๋‹ค.

 

โ–ถ polls_api/permissions.py

class IsVoter(permissions.BasePermission):
    def has_object_permission(self, request, view, obj):
        return obj.voter == request.user # ์˜ค๋ธŒ์ ํŠธ์˜ ์†Œ์œ ์ž๊ฐ€ ์š”์ฒญํ•œ ์‚ฌ์šฉ์ž ์ธ๊ฐ€? ๋งž์œผ๋ฉด True

permissions์—์„œ ๋ฏธ๋ฆฌ ์ •์˜ํ•ด๋‘” ํ•จ์ˆ˜๋ฅผ ๊ฐ€์ ธ๋‹ค ์‚ฌ์šฉํ–ˆ๋˜ ๊ฒƒ์ด๋‹ค.

 

views.py๋ฅผ ์ •์˜ํ–ˆ์œผ๋‹ˆ, urls.py์— path๋ฅผ ์ง€์ •ํ•˜๋Š” ๊ฒƒ๋„ ์žŠ์ง€ ๋ง์•„์•ผ ํ•œ๋‹ค.

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

 

๊ฒฐ๊ณผ

Question ํŽ˜์ด์ง€์™€ ๊ฑฐ์˜ ๋น„์Šทํ•˜๋‹ค. ๋˜‘๊ฐ™์ด CRUD๋„ ์ž์œ ๋กญ๋‹ค.

 

 


 

๐ŸŸจ Validation

๋ช‡ ๊ฐ€์ง€ ์˜ค๋ฅ˜๋ฅผ ์ˆ˜์ •ํ•ด๋ณด๊ฒ ๋‹ค.

 

1. question๊ณผ user๊ฐ€ ๊ฐ™์€ ๊ฐ’์ด ๋“ค์–ด์™”์„ ๋•Œ 400 ์—๋Ÿฌ๊ฐ€ ์•„๋‹Œ 500 ์—๋Ÿฌ๊ฐ€ ๋‚˜ํƒ€๋‚˜๋Š” ํ˜„์ƒ ๋ฐฉ์ง€

 

โ–ถ polls_api/serializers.py

from rest_framework.validators import UniqueTogetherValidator

class VoteSerializer(serializers.ModelSerializer):

    class Meta:
        model = Vote
        fields = ['id', 'question', 'choice', 'voter']
        validators = [
            UniqueTogetherValidator(
                queryset = Vote.objects.all(),
                fields=['question', 'voter']
            )
        ]

Serializer์— validators๋ฅผ ์ถ”๊ฐ€ํ•ด์ฃผ๋ฉด 400 Bad request  ์—๋Ÿฌ๋กœ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๋‚˜ํƒ€๋‚œ๋‹ค.

 

ํ•˜์ง€๋งŒ ์ด๋ฒˆ์—๋Š” ์—๋Ÿฌ๋ฉ”์‹œ์ง€๊ฐ€ ์ด์ƒํ•˜๋‹ค.

"voter" : "This field is required."

๋ผ๊ณ  ๋‚˜์˜ค์ง€๋งŒ, voter๋Š” ์ƒ๊ด€์ด ์—†๊ณ  question๊ณผ voter์˜ UniqueVailidation์— ๋Œ€ํ•œ ์˜ค๋ฅ˜๋กœ ๋ฐ”๊ฟ”์ค˜์•ผํ•œ๋‹ค.

 

โ–ถ polls_api/views.py

from rest_framework import status
from rest_framework.response import Response

class VoteList(generics.ListCreateAPIView):
    serializer_class = VoteSerializer
    permission_classes = [permissions.IsAuthenticated]
    
    def get_queryset(self, *args, **kwargs):
        return Vote.objects.filter(voter=self.request.user)
    
    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)

views.py์—์„œ VoteList ํด๋ž˜์Šค์— create ํ•จ์ˆ˜๋ฅผ ์˜ค๋ฐ”๋ผ์ด๋”ฉํ•ด์ฃผ์–ด์•ผํ•œ๋‹ค.

์—๋Ÿฌ์˜ ์ด์œ ๊ฐ€ ๋ถ€๋ชจ ํด๋ž˜์Šค์—์„œ ์ •์˜๋œ create ๋ฉ”์„œ๋“œ์— ์žˆ๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.

 

์šฐ๋ฆฌ๊ฐ€ ์˜ค๋ฒ„๋ผ์ด๋”ฉ perform_create ๋ฉ”์„œ๋“œ์— user ๋ฐ์ดํ„ฐ๋ฅผ ๋„ฃ์–ด์ฃผ์—ˆ๋ฐ,

create ๋ฉ”์„œ๋“œ์—์„œ๋Š” .is_valid๊ฐ€ .perform_create ๋ฉ”์„œ๋“œ๋ณด๋‹ค ๋จผ์ € ์‹คํ–‰๋˜๊ธฐ ๋•Œ๋ฌธ์— user ๊ฐ’์ด ์—†๋‹ค๊ณ  ์—๋Ÿฌ๊ฐ€ ๋œจ๋Š” ๊ฒƒ์ด๋‹ค.

 

๋”ฐ๋ผ์„œ ์œ„์ฒ˜๋Ÿผ createํ•จ์ˆ˜์—์„œ perfrom_create ๋ฉ”์„œ๋“œ์—์„œ user ๊ฐ’์„ ๋„ฃ์ง€์•Š๊ณ ,

์• ์ดˆ์— new_data ๋ณ€์ˆ˜๋ฅผ ๋งŒ๋“ค์–ด ์š”์ฒญ๋ฐ›์€ user ๊ฐ’์„ ๊ทธ๋Œ€๋กœ ๋„ฃ์–ด์ค€๋‹ค.

 

+ ๊ทธ๋ฆฌ๊ณ , serializer์—๊ฒŒ voter๊ฐ’์ด ์ž˜ ์ „๋‹ฌ๋˜๋„๋ก VoteSerializer์— vote์˜ Readonly ์ œํ•œ๋„ ์—†์• ์ค˜์•ผํ•œ๋‹ค.

 

2. ๋กœ๊ทธ์ธ๋œ user๊ฐ€ ์ž์‹ ์˜ vote ๊ฐ์ฒด๋ฅผ ๋‹ค๋ฅธ user์˜ ์ด๋ฆ„์œผ๋กœ ๋ฐ”๊ฟ€ ์ˆ˜ ์žˆ๋Š” ๋ฌธ์ œ

 

โ–ถ polls_api/views.py

class VoteDetail(generics.RetrieveUpdateDestroyAPIView):
    queryset = Vote.objects.all()
    serializer_class = VoteSerializer
    permission_classes = [permissions.IsAuthenticated, IsVoter] #ํˆฌํ‘œํ•œ ์‚ฌ๋žŒ๋งŒ CRUD ๊ฐ€๋Šฅํ•ด์•ผํ•จ

    def perform_update(self, serializer):
        serializer.save(voter=self.request.user)

perform_update ๋ฉ”์„œ๋“œ๋ฅผ ์˜ค๋ฒ„๋ผ์ด๋”ฉํ•œ๋‹ค.

์ด๋ ‡๊ฒŒํ•˜๋ฉด, ์œ ์ €๊ฐ€ ๋‹ค๋ฅธ ์œ ์ €์˜ ์ด๋ฆ„์„ ๋„ฃ์„ ์ˆœ ์žˆ์–ด๋„ ๋กœ๊ทธ์ธ๋œ ์œ ์ €์˜ ์ด๋ฆ„์œผ๋กœ ๊ณ ์ •๋˜์–ด ๋“ค์–ด๊ฐ€๊ธด ํ•œ๋‹ค..

 

 

3. vote๋ฅผ ์ˆ˜์ •ํ• ๋•Œ question๊ณผ choice๊ฐ€ ์„œ๋กœ ๋งž์ง€ ์•Š๋Š”๋ฐ ์ˆ˜์ •๋˜๋Š” ๋ฌธ์ œ

โ–ถ polls_api/serializers.py

class VoteSerializer(serializers.ModelSerializer):

    def validate(self, attrs): # validation ์ˆ˜ํ–‰, attrs์— ๋ฐ์ดํ„ฐ๋ฅผ ๋‹ด์•„์˜จ๋‹ค.
        if attrs['choice'].question.id != attrs['question'].id:
            raise serializers.ValidationError("Qeustion๊ณผ Choice๊ฐ€ ์กฐํ•ฉ์ด ๋งž์ง€ ์•Š์Šต๋‹ˆ๋‹ค.")
        return attrs

    class Meta:
        model = Vote
        fields = ['id', 'question', 'choice', 'voter']
        validators = [
            UniqueTogetherValidator(
                queryset = Vote.objects.all(),
                fields=['question', 'voter']
            )
        ]

VoteSerializer์— validate ํ•จ์ˆ˜๋ฅผ ์ถ”๊ฐ€ํ•ด์ฃผ์–ด์•ผํ•œ๋‹ค. ์ด๋Š” validation์„ ์ˆ˜ํ–‰ํ•˜๋Š” ํ•จ์ˆ˜์ด๋‹ค.

Serializer์—์„œ validation์„ ์ˆ˜ํ–‰ํ•จ์œผ๋กœ์จ ์˜ค๋ฅ˜๊ฐ€๋‚˜๋ฉด ํ•ด๋‹น ์˜ค๋ฅ˜ ํ…์ŠคํŠธ๋ฅผ ๋„์šธ ์ˆ˜๊ฐ€ ์žˆ๋‹ค.

 


 

๐ŸŸง Testing

์ปดํ“จํ„ฐ๊ฐ€ ์Šค์Šค๋กœ ํ…Œ์ŠคํŠธ๋ฅผ ๋Œ๋ ค์ฃผ๋Š” ์ž๋™ํ™” ๊ธฐ๋Šฅ

django์—์„œ ์ง€์›ํ•˜๋Š” TestCase์™€ rest_framework์˜ APITestCase๋ฅผ ์ด์šฉํ•ด์„œ ์ž๋™ํ™” ํ…Œ์ŠคํŠธ ํŒŒ์ผ์„ ๋งŒ๋“ค์–ด๋ณธ๋‹ค!

 

Test ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ• ๋•Œ๋Š” ๋‹ค์Œ์„ ์ฃผ์˜ํ•˜๋ฉด์„œ ์ž‘์„ฑํ•œ๋‹ค.

  • ์ •์˜๋œ ํ…Œ์ŠคํŠธ ํ•จ์ˆ˜์˜ ์ด๋ฆ„์€ "test_"๋กœ ์‹œ์ž‘ํ•ด์•ผํ•˜๋ฉฐ, ๊ทธ ์ด๋ฆ„์„ ์ž˜ ๋ช…์‹œํ•ด์•ผ ํ•œ๋‹ค.
  • TestCase๋Š” Test๋ฅผ ์œ„ํ•œ ๋ณ„๋„์˜ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋ฅผ ์ด์šฉํ•œ๋‹ค.
  • ๊ฐ ํ…Œ์ŠคํŠธ๋Š” ๋ชจ๋‘ ๋ณ„๊ฐœ๋กœ ๋™์ž‘ํ•œ๋‹ค.

ํ…Œ์ŠคํŠธ๋ฅผ ์œ„ํ•œ assert ๋ฉ”์„œ๋“œ

  • .assertEqual() : ๋‘ ์ธ์ž๊ฐ€ ์„œ๋กœ ๊ฐ™์€์ง€ ํ™•์ธํ•œ๋‹ค. == ์™€ ๊ฐ™์ด ๊ฒ€์‚ฌํ•œ๋‹ค.
  • .assertIsNotNone() : ์ธ์ž๊ฐ€ None์ด ์•„๋‹ˆ๋ฉด True๋ฅผ ๋ฆฌํ„ดํ•œ๋‹ค.
  • .assertTrue() : ์ธ์ž๊ฐ’์ด True์ธ์ง€ ํ™•์ธํ•œ๋‹ค.
  • .assertLess(): ๋‘ ๊ฐœ์˜ ์ธ์ž๋ฅผ ๋ฐ›๋Š”๋‹ค. ์˜ค๋ฅธ์ชฝ์˜ ์ธ์ž๊ฐ€ ์™ผ์ชฝ์˜ ์ธ์ž๋ณด๋‹ค ์ž‘์€์ง€ ํ™•์ธํ•œ๋‹ค.

ํ…Œ์ŠคํŠธ์˜ ๋™์ž‘ ํ™•์ธ์€ shell์—์„œ

>>> python manage.py test

๋กœ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.

 

โ–ถ polls_api/tests.py

from django.test import TestCase
from polls_api.serializers import QuestionSerializer

class QuestionSerializerTestCase(TestCase):
    # def test_a(self): # test_ ๋กœ ์‹œ์ž‘ํ•˜๋Š” ํ•จ์ˆ˜๋งŒ ํ…Œ์ŠคํŠธ๋ผ๊ณ  ์ธ์‹ํ•จ # ์„ฑ๊ณตํ•˜๋ฉด . ์‹คํŒจํ•˜๋ฉด F

    def test_with_valid_data(self):
        serializer = QuestionSerializer(data={'question_text' : 'abc'})
        self.assertEqual(serializer.is_valid(), True) # question_text๋ฅผ ์ž…๋ ฅํ•˜๊ณ  createํ• ๋•Œ, is_validํ•œ ์ƒํ™ฉ์ธ๊ฐ€?
        new_question = serializer.save() # id๊ฐ€ ์ž๋™์ €์žฅ๋  ๊ฒƒ์ž„
        self.assertIsNotNone(new_question.id) # id๊ฐ€ ๋น„์–ด์žˆ๋Š”๊ฐ€?

    # question_text๊ฐ€ ๋น„์–ด์žˆ๋„๋ก ์ƒ์„ฑํ•˜๋ฉด is_valid()๊ฐ€ False์ธ๊ฐ€?
    def test_with_invalid_data(self):
        serializer = QuestionSerializer(data={'question_text' : ''})
        self.assertEqual(serializer.is_valid(), False)

QuestionSerializer๋ฅผ ํ…Œ์ŠคํŠธํ•˜๊ธฐ ์œ„ํ•œ ์ฝ”๋“œ์ด๋‹ค.

Test๋ฅผ ํ•ด๋ณด๋‹ˆ, ๋ชจ๋‘ ํŒจ์Šคํ–ˆ๋‹ค.

 

class VoteSerializerTest(TestCase):

    def setUp(self): # ํ…Œ์ŠคํŠธ๊ฐ€ ์‹คํ–‰๋ ๋•Œ๋งˆ๋‹ค ํ•œ๋ฒˆ์”ฉ ๋ฐ˜๋ณต๋˜์–ด ์‹คํ–‰ํ•จ, *self๋ฅผ ์‚ฌ์šฉํ•จ์— ์ฃผ์˜* ํ…Œ์ŠคํŠธ๋งˆ๋‹ค ๋…๋ฆฝ์ ์ˆ˜ํ–‰
        self.user = User.objects.create(username='testuser')
        self.question = Question.objects.create(
            question_text ='abc',
            owner = self.user
        )
        self.choice1 = Choice.objects.create(
            question=self.question,
            choice_text = '1'
        )

    # create vote Test
    def test_vote_serializer(self):
        data ={
            'question' : self.question.id,
            'choice' : self.choice1.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.choice1)
        self.assertEqual(vote.voter, self.user)


    # question, user์˜ UniqueValidation Test
    def test_vote_serializer_with_duplicate_vote(self): # ๋ฉ”์†Œ๋“œ๋ฅผ ์ž˜ ์ ์–ด์ค˜์•ผ ์—๋Ÿฌ๋ฐœ์ƒํ–ˆ์„ ๋•Œ ์•Œ๊ธฐ ์‰ฌ์›€

        choice2 = Choice.objects.create( # ์ƒˆ๋กญ๊ฒŒ ๋“ค์–ด๊ฐˆ Vote์šฉ
            question=self.question,
            choice_text = '2'
        )
        Vote.objects.create(question=self.question, choice=self.choice1, voter=self.user) # ๋ฏธ๋ฆฌ ํ•˜๋‚˜ ๋งŒ๋“ค์–ด๋‘ 

        data ={
            'question' : self.question.id,
            'choice' : choice2.id,
            'voter' : self.user.id
        }
        serializer = VoteSerializer(data= data)
        self.assertFalse(serializer.is_valid()) # question๊ณผ user๊ฐ€ ๋™์ผํ•œ vote๋ฅผ ๋˜ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ์„๊นŒ?

    # question๊ณผ choice์˜ unmatch Test
    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())

VoteSerializer๋ฅผ ํ…Œ์ŠคํŠธํ•˜๋Š” ์ฝ”๋“œ์ด๋‹ค.

์—„์ฒญ ๊ธธ์ง€๋งŒ ๋Œ€๋ถ€๋ถ„ ๋ณ€์ˆ˜๋“ค์„ ์ดˆ๊ธฐํ™”ํ•˜๋Š” ๋‚ด์šฉ์ด๊ณ , ์ง๊ด€์ ์ด๋‹ค.

 

์ค‘์š”ํ•œ๊ฑด, setUp() ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ–ˆ๋‹ค.

  • setUp() ๋ฉ”์„œ๋“œ๋Š” ๊ฐ ํ…Œ์ŠคํŠธ๊ฐ€ ์‹œ์ž‘๋˜๊ธฐ ์ „์— ํ•œ๋ฒˆ์”ฉ ์‹คํ–‰๋˜๋Š” ๋ฉ”์„œ๋“œ์ด๋‹ค.
  • ๋ฐ˜๋ณต๋˜๋Š” ์ฝ”๋“œ๋‚˜ ์ž์ฃผ ์ดˆ๊ธฐํ™”๋˜๋Š” ๋ณ€์ˆ˜๋ฅผ ๋„ฃ์–ด์ฃผ๋ฉด ์ข‹๋‹ค.
  • ์‚ฌ์šฉ๋œ ๋ณ€์ˆ˜๋Š” ๊ทธ ํด๋ž˜์Šค์•ˆ์—์„œ self.๋ณ€์ˆ˜๋ช… ์„ ํ†ตํ•ด ๋ถˆ๋Ÿฌ์™€์•ผํ•œ๋‹ค.

 

์ด๋ฒˆ์—” APITestCase๋ฅผ ์ด์šฉํ•˜์—ฌ View๋ฅผ ํ…Œ์ŠคํŠธํ•œ ์ฝ”๋“œ์ด๋‹ค.

# View์˜ ๊ฒฝ์šฐ APITestCase๊ฐ€ ํ•„์š”
class QuestionListTest(APITestCase):
    def setUp(self):
        self.question_data = {'question_text' : 'some question'}
        self.url = reverse('question-list') # ๋ฉ”์†Œ๋“œ์•ˆ์—์„œ๋Š” reverse.lazy๊ฐ€ ์•„๋‹Œ reverse ์‚ฌ์šฉํ•˜๊ธฐ.

    def test_create_question(self):
        user = User.objects.create(username = 'testuser', password ="testpass")
        self.client.force_authenticate(user=user) # ์‚ฌ์šฉ์ž๋ฅผ ๋กœ๊ทธ์ธ ์‹œํ‚ค๋Š” ๊ธฐ๋Šฅ (APITestCase ๊ธฐ๋Šฅ)
        response = self.client.post(self.url, self.question_data) # ํ•ด๋‹น ๋ฐ์ดํ„ฐ๋ฅผ url๋กœ post ์š”์ฒญ

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

        question = Question.objects.first()
        self.assertLess((timezone.now() - question.pub_date).total_seconds(), 1) # assertLess๋Š” ๋’ค์˜ ์ธ์ž๋ณด๋‹ค ์ž‘์€๊ฐ€? ํ™•์ธํ•จ

    # ์‚ฌ์šฉ์ž ๋กœ๊ทธ์ธ์—†์ด question์„ ๋งŒ๋“ค์—ˆ์„ ๋•Œ
    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_questions(self):
        question = Question.objects.create(question_text='Question1')
        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)

์‚ฌ์šฉ์ž๋ฅผ ๋กœ๊ทธ์ธ ์ƒํƒœ๋กœ ๋‘๋Š” ํ•จ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ์œ„ํ•ด APITestCase์˜ ์ƒ์†์ด ํ•„์š”ํ–ˆ๋‹ค.

 

+ ํŒŒ์ด์ฌ์—์„œ ์ž‘์„ฑํ•œ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ์˜ ์ปค๋ฒ„๋ฆฌ์ง€๋ฅผ ์ธก์ •ํ•  ์ˆ˜ ์žˆ๋Š” ๋„๊ตฌ๊ฐ€ ์žˆ๋‹ค.

>>> pip install coverage

>>> coverage run manage.py test

>>> coverage report

๋‘ ๋ฒˆ์งธ ๋ช…๋ น์–ด๋กœ test๋ฅผ ์‹คํ–‰ํ•˜๊ณ , ์„ธ ๋ฒˆ์งธ ๋ช…๋ น์–ด๋กœ test์˜ ๊ฒฐ๊ณผ ์ปค๋ฒ„๋ฆฌ์ง€๋ฅผ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.

 

๊ฒฐ๊ณผ

serializer์™€ views ํ…Œ์ŠคํŠธ๊ฐ€ ๋ถ€์กฑํ•œ๊ฑธ ์•Œ ์ˆ˜ ์žˆ๋‹ค!

 


๐Ÿค” ๊ณต๋ถ€ํ•˜๋ฉด์„œ ์–ด๋ ค์› ๋˜ ๋‚ด์šฉ

์˜ค๋Š˜์€ ์ž‘์„ฑํ•œ ์ฝ”๋“œ๋ฅผ ๋ณด์™„ํ•˜๊ฑฐ๋‚˜ ํ…Œ์ŠคํŠธํ•ด๋ณผ ์ˆ˜ ์žˆ์—ˆ๋‹ค.

์–ด๋–ป๊ฒŒ๋ณด๋ฉด ๊ฐ€์žฅ ๊ท€์ฐฎ์ง€๋งŒ ์ค‘์š”ํ•œ ์ž‘์—…์ด๋ผ๊ณ  ์ƒ๊ฐํ•œ๋‹ค.

๋ฐฑํ”„๋กœ ์™„์„ฑ๋œ ์ฝ”๋“œ๋Š” ์—†๋Š” ๋ฒ•์ด๋‹ˆ๊นŒ.. ๐Ÿ˜‡

'TIL' ์นดํ…Œ๊ณ ๋ฆฌ์˜ ๋‹ค๋ฅธ ๊ธ€

[TIL]KDT_20230502  (0) 2023.05.02
[TIL]KDT_20230501  (0) 2023.05.01
[TIL]KDT_20230427  (0) 2023.04.27
[TIL]KDT_20230426  (0) 2023.04.26
[TIL]KDT_20230425  (0) 2023.04.25