[기업협업] 퀀텀AI -Day 19-

제갈창민·2022년 1월 17일
0

기업협업[퀀텀AI]

목록 보기
13/18
post-custom-banner

D

Delay 되었던 유닛 테스트를 오늘은 어느정도 마무리를 해야만 했다. 다음주 목요일에 협업이 마무리되기 때문에 현재까지 작성된 API에 대한 테스트 코드도 작성해야하고, 프론트와 최종확인까지 마쳐야 하기 때문이다. 사용된 테스트 코드는 다음과 같다.

class SchoolTestCase(APITestCase):
    
    def setUp(self):
        User = get_user_model()
        self.user = User.objects.create_user(username='test', password='Password!')
        self.access = AccessToken.for_user(self.user)
        self.client.credentials(HTTP_AUTHORIZATION=f'Bearer {self.access}')

    def test_school_viewset(self):
        data ={
            'school_name' : 'test',
        }

        response = self.client.post('/school/', data, format='json')
        self.assertEqual(response.status_code, status.HTTP_201_CREATED)

	response = self.client.get('/school/', data, format='json')
        self.assertEqual(response.status_code, status.HTTP_200_OK)

여기에서 get에 추가된 '검색' 기능을 사용했을 때, 그리고 serializer에서 추가 된 정보에 대한 결과값을 확인하기 위한 테스트 코드를 작성하는 것이 목표다.

R

response 를 한 줄 더 추가하기 전에 먼저 viewserializer를 먼저 살펴 보자.

views.py
class SchoolGradesViewSet(ModelViewSet):
    permission_classes = [IsAdminUser]
    serializer_class = SchoolGradesSerializer
    pagination_class = SchoolGradesPagination

def get_queryset(self):
   school_name = self.request.query_params.get('school_name')

   if school_name:
      queryset = SchoolGrades.objects.annotate(school_name=F('school__school_name')).filter(school_name__icontains=school_name)
        
   else:
      queryset = SchoolGrades.objects.all()
            
   return queryset
   
   
serializers.py
class SchoolGradesSerializer(serializers.ModelSerializer):
    school_info = serializers.SerializerMethodField()
    
    class Meta:
        model = SchoolGrades
        fields = ['id', 'school_id', 'school_grade', 'school_info']'school_info'는 serializer 에서 추가된 정보이므로 나중에 다룬다.
        ※ 이번 포스팅에서는 '검색' 기능 결과에 대한 실패 경험만을 다룬다.

    def get_school_info(self, obj):
        return School.objects.filter(id=obj.school_id).values('school_name')

SchoolGrades 테이블은 School 테이블을 정참조 하고 있다. 이제 'school_name'으로 검색 했을 때의 테스트 코드를 덧붙이자.

response = self.client.get('/school/?school_name=te', format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)

이렇게만 쓰면 깔끔하게 OK가 뜰 것이다. 하지만 내가 하고자 하는 건 이게 아니다. 단순히 'status_code' 만으로 테스트를 하면 빈 값이 나오더라도 200_OK가 되기 때문에 의미가 없다. 그래서 실제로 출력되는 값을 비교해보아야 한다.

self.assertEqual(response.data, {'id':1, 'school_id':1, 'school_grade':'상', 'school_info'=[{etc..}]}

F

From now on(지금부터) 문제는 발생한다. response.dataprint 해보면 다음과 같은 결과값이 등장한다.

OrderedDict([('count', 1), ('next', None)[332 chars])])])

OrderedDict가 내 금요일을 소멸시킨 장본인 되시겠다. 이녀석은 오늘의 TIL에서 더 알아보도록 하고 다시 코드로 돌아가자. OrderedDict를 만난 후로 다양한 시도를 해보았다.

1. self.assertEqual(response.data['id', 'shop_id'...], {...})
2. self.assertEqual(response.data.first, {...})
3. self.assertEqual(response, {...})
4. self.assertEqual(response.data, ([(...)]))
5. response_data = json.loads['data']
   self.assertEqual(response_data, {...})
6. self.assertEqual(response.data, set({...}))
7. self.assertEqual(**response.data, {...})
......

당연히 모조리 실패 했다. 심지어 입력값을 최고한으로 줄여서 print된 출력값을 토씨하나 안빠지고 똑같이 복붙을 해봤는데도 일치하지 않는다는 에러가 떴다(이건 진짜 이상했음). 결국 퇴근시간이 될 때까지 해결하지 못했고, stackoverflow에 질문을 남겨두고 금요일을 마무리 했다.

TIL

서순하는 Dict, 순서를 아는 OrderedDict

알다시피 파이썬에서 dict 형태는 순서(index)가 정해져 있지 않다. 그래서 아래와 같은 로직도 성립을 한다.

A = {'a':1, 'b':2, 'c':3}
B = {'a':1, 'c':3, 'b':2}
A == B
True

하지만 순서를 지키는 OrderedDict를 사용하면 순서까지 맞춰야 True를 반환한다.
파이썬의 collactions 모듈에서 임포트 해야하는 OrderedDict를 단순히 순서가 있는 dict 형식으로만 사용할 생각이면 파이썬 3.6버전 보다 상위버전을 쓰고 있을 시, 그냥 dict 를 사용해도 된다. 기존 dict에 순서를 포함하는 기능을 업그레이드 해 준 것이다. 하위 버전의 파이썬을 쓰거나, 조금 더 엄격한 동등성 비교를 필요로 할 때에는 OrderedDict 를 사용 할 수 있다.

a = OrderedDict({'a':1})

serializer 의 field 추가 기능

DRF에 대한 초기 포스팅에서 serializer 에 추가적으로 정보를 찾아서 field에 추가해주는 로직을 구현한 적이 있다. 그때와 지금이 달라진 점은 class Meta 의 field 에서 더 이상 __all__ 을 쓰지 않고 각각의 필요한 field 명을 일일이 작성해준다는 점이다. 만약 MethodField 를 지정해 놓고 함수처리를 하지 않는다 거나, 함수 처리까지 다 했지만, field 에 해당 field 명을 넣지 않는 다거나, 일치하지 않는 field 명이 존재 한다면 무조건 에러가 발생한다. 그래서 추가 정보를 출력하는 로직이 구현된 serializer의 모습은 항상 다음과 같은 포맷을 유지한다.

class SchoolGradesSerializer(serializers.ModelSerializer):
    school_info = serializers.SerializerMethodField()
    
    class Meta:
        model = SchoolGrades
        fields = ['id', 'school_id', 'school_grade', 'school_info']

    def get_school_info(self, obj):
        return School.objects.filter(id=obj.school_id).values('school_name')

알고 있기로는 함수명이나 클래스명은 컨벤션 때문이지 사실상 코드를 작성하는 개발자의 마음대로 지정할 수 있다고 배웠으나, DRF에서는 그럴 수 없다. 만약 def get_school_info 에서 한 글자라도 빠지거나 오타가 발생하면 함수가 작동하지 않는다. 이는 MethodField 나 fields=[] 도 마찬가지다. 세 부분이 똑같이 일치해야 로직이 정상적으로 작동하는 것이다.

편의와 편리 두 마리 토끼를 다 잡고자 하는 DRF 이지만, 그 모든 혜택을 누리려면 토끼를 잡으려 하는 사용자의 경험치가 상당히 많이 요구되는 프레임워크 인 것 같다.

profile
자기계발 중인 신입 개발자
post-custom-banner

0개의 댓글