본문 바로가기
Django/Django REST framework

DRF 테스트코드 개발 도전기

by hyun-am 2021. 12. 17.

Test Code를 작성하는 이유

먼저 테스트 코드에 대해서 설명하기 전에 Jacob Kaplan-Moss라는 분이 테스트가 없는 코드는 잘못 설계된 코드입니다. 라는 말을 했습니다.

또한 테스트 코드를 작성하면 좋은 점은 잘 동작하고, 깔끔한 코드를 얻기위해서, 또한 시간을 단축해 줍니다. 저도 마찬가지로 테스트 코드를 작성하기 전에 매일.. Postman으로 테스트를 했는데 이 과정은 매우 번거로울 뿐 아니라 매우 비효율 적이라고 생각을 했습니다. 그래서 테스트코드를 작성하면 Postman으로 1개의 api를 테스트 할 시간에 수십개~수백개의 테스트 케이스를 테스트 할 수 있다고 생각 해서 Test 코드를 개발 했습니다.

DRF Test 코드 작성하기

setUp 작성하기

DRF에서는 다양한 test를 위한 도구들이 제공되어 있는데 저는 주로 APITestCase클래스를 이용해서 테스트를 진행했습니다. 이것을 이용하면 self.client라는 것을 이용하면 postman에서 API를 호출하는 것 처럼 테스트 코드 상에서도 사용할 수 있습니다.

테스트 케이스를 작성할 때 제일 먼저 setUp이라는 method를 작성하겠습니다.

class TestCaseName(APITestCase):
    def setUp(self):
        self.factory = APIRequestFactory()
        self.test_url = "테스트하고싶은url"
        self.register_url = "회원가입url" # HTTP_AUTHORIZATION을 위해 사용
				self.login_url = "로그인url" # HTTP_AUTHORIZATION을 위해 사용

여기서 setUp이 하는 일은 나중에 작성될 하나 하나의 테스트 케이스 들을 실행하기 전에 제일 먼저 실행되는 세팅같은 개념이라고 생각하면 되겠습니다.

다양한 API기능들을 테스트하기 위한 코드를 작성하기 전에 인증을 위한 세팅에 대해서 설명하겠습니다. 여기서 적는 코드들은 아직 factoryboy라는 좋은 테스트 데이터를 생성해주는 라이브러리를 발견하기 전에 작성한 코드입니다.

회원가입하기

self.user_data = {
            "email": "test@test.com",
            "password": "test001!",
            "nickname": "test_nickname",
						"회원가입에필요한데이터" : "기타등등등",
	        }
self.client.post(self.register_url, data=self.user_data)

먼저 이런식으로 회원가입에 필요한 데이터들만 입력해서 회원가입을 해 user를 생성하겠습니다.

로그인하기

self.acces_token = self.client.post(
            self.auth_url,
            {
                "email": self.user_data.get("email"),
                "password": self.user_data.get("password"),
            },
        ).data.get("token")

이런식으로 회원가입을 한 이메일과 비밀번호를 그대로 입력해서 인증에 필요한 token을 acces_token에 저장하겠습니다.

인증하기

self.client.credentials(
            HTTP_AUTHORIZATION=f"Bearer {self.acces_token['access_token']}"
        )

마지막으로 저는 토큰을 인증하기 위해 다음과 같은 credentials을 진행했습니다.

그룹을 만드는 테스트 케이스

mealligram기능 중 하나인 그룹을 만드는 기능이 제일 기초여서 이 부분에 대해서 테스트코드를 짠 것을 기반으로 쭉쭉쭉 테스트 케이스를 작성했습니다.

테스트를 하고싶은 코드들의 이름 맨 앞에는 test를 꼭 붙여야 합니다. 그래야 자동으로 테스트 코드가 실행할때 테스트 코드가 실행이 됩니다.

# 그룹을 생성하는 코드 테스트 -> 만약 테스트 코드가 많아지면 헷갈릴 수 있으니 이름으로 부족할 것 같으면 주석 작성
def test_create_group(self):
    data = {
            "그룹을": False,
            "생성할": ["TestTest"],
            "때": "TestTest",
            "필요한": "TestTest",
            "데이터": "😄",
            "들입니다": [
                "TestTest",
                "TestTest",
            ],
            "ㅎㅎ": "TestTest",
        }

		response = self.client.post(self.create_url, data=data, format="json")
		self.assertEqual(response.status_code, status.HTTP_201_CREATED)

여기서 assertEqual의 기능은 아래와 같습니다.

assertEqual(first, second, msg=None)first와 second가 같은지 테스트합니다. 비교한 값이 같지 않으면 테스트는 실패할 것입니다. 추가로, 만약 first와 second가 정확히 같은 형(type)이고 list, tuple, dict, set, frozenset, str 이거나 [addTypeEqualityFunc()](<https://docs.python.org/ko/3/library/unittest.html#unittest.TestCase.addTypeEqualityFunc>)에 등록된 서브 클래스 형 중 하나일 경우 더 유용한 기본 에러 메시지를 생성하기 위해 형-특화(type-specific) 동등성 함수가 불릴 것입니다(형-특화 메서드 목록을 참고하십시오). 버전 3.1에서 변경: 형-특화 동등성 함수가 자동으로 불리도록 추가 버전 3.2에서 변경: 문자열 비교를 위해서 [assertMultiLineEqual()](<https://docs.python.org/ko/3/library/unittest.html#unittest.TestCase.assertMultiLineEqual>)를 기본 형-특화 동등성 함수에 추가

참고 링크 : https://docs.python.org/ko/3/library/unittest.html#module-unittest

이런식으로 작성한 다음 아래와 같은 명령어를 실행시키면 결과가 나옵니다.

# 전체 확인
python manage.py test
# 앱 단위로 확인
python manage.py test [앱이름]

만약 test가 성공한다면 Ran 1 tests in 000s OK라고 나올것이고 코드상 에러가 있으면 error = 1이 나오고 만약 테스트 예상 값이 다르면 failure =1이 나옵니다.

Factory Boy를 이용해서 테스트 데이터 쉽게 만들기

Factoryboy를 사용하게 된 이유

테스트 코드를 작성하다가 위에 코드를 보면 유저 하나의 인증 데이터를 얻기위해서 13~20줄 정도의 코드를 작성해야 합니다. 따라서 만약에 method를 작성하지 않으면 유저10명이 방에 들어가거나 인증을 얻는 코드를 작성하려면 100줄 이상이 넘는 코드를 작성 할 수도 있어서 factoryboy를 통해 쉽게 데이터를 생성하자는 생각을 했습니다.

factoryboy란

factoryboy를 이용하면 새 인스턴스를 가져오는 것은 물론 필드들을 재 정의할 수 있게 도와주고 더욱 쉽게 개체들을 만들 수 있게 도와주는 django library입니다.

Userfactory 작성하기

user를 더욱 쉽게 생성하기위한 userfactory를 생성하는 과정을 소개하겠습니다.

먼저 유저 앱에 factory라는 폴더를 만든 후 user_factory.py라는 파일을 생성했습니다.

import factory.fuzzy
import uuid
from django.utils import timezone
from users.models import User, Device
from group.models import 필요한 모델들
from django.contrib.auth.hashers import make_password
class UserFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = User
        django_get_or_create = ["nickname"]

    nickname = factory.Faker("name")
    password = make_password("test001!")
    email = factory.lazy_attribute(lambda a: f"{a.nickname.strip()}@test.com")
    username = factory.lazy_attribute(lambda a: f"{a.nickname.strip()}@test.com")
    createdAt = timezone.now()
    updatedAt = timezone.now()

먼저 이런식으로 UserFactory를 작성했습니다. 여기서 보면 unique한 값은 django_get_or_create에 넣어서 중복생성을 방지했고, email과 username같이 중복을 피해 변경이 필요한 것들은 lazy_attribute를 통해 규격을 세팅해 주었습니다.

또한 make_password를 사용한 이유는 django는 회원가입을 하면 자동으로 비밀번호가 hash로 생성되어서 equal을 하기 어려운데 이것을 이용하면 쉽고간단하게 비교할 수 있습니다.

또한 유저를 생성하면 필요하거나 같이 생성되는 항목도 같이 SubFactory를 이용해 작성했습니다.

class UserDeviceFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = Device
        django_get_or_create = ["id"]

    user = factory.SubFactory(UserFactory)
    id = factory.Faker("name")
    필요한 = "데이터"
    기타 = "등등"
    입니다 = factory.Faker("pystr")

또한 facker를 이용하면 굳이 지정 없이 랜덤으로 값을 쉽게 생성해 주어서 쉽고 간단하게 테스트 데이터들을 생성할 수 있습니다.

UserFactory 테스트 코드에 적용하기

위에 작성했던 group기능에 있는 test.py로 가겠습니다.

setup위에 인증할때 필요한 메서드를 그대로 작성하겠습니다.

def change_authorization(self, client_user):
        acces_token = self.client.post(
            self.auth_url,
            {
                "email": client_user.email,
                "password": "test001!",
            },
        ).data.get("token")
        self.client.credentials(
            HTTP_AUTHORIZATION=f"Bearer {acces_token['access_token']}"
        )

client_user정보만 받으면 그 유저의 인증으로 바꿀 수 있는 메서드를 작성했습니다.

그러면 위에서 setUp에 작성한 부분도 아래와 같이 바꿔서 작성할 수 있습니다.

UserDeviceFactory.create_batch(1, user__nickname="test_nickname")
host_user = User.objects.get(nickname="test_nickname")
self.change_authorization(host_user)

여기서 user__nickname부분을 잘 봐야하는데 UserDeviceFactory에서 subfactory로 user를 설정해서 __를 통해 접근할 수 있게 도와줍니다.

만약 5명의 유저를 생성하고 nickname은 랜덤으로 생성하고 싶으면 다음과 같이 코드를 작성하면 되겠습니다.

UserDeviceFactory.create_batch(5)

이렇게 하면 유저에 어떻게 접근하지????라는 생각을 할 수 있습니다. 그러면 현재 인증을 얻은 유저를 제외하고 유저정보를 받아오는 코드를 다음과 같이 작성하면 쉽게 구할 수 있습니다.

for client_user in User.objects.exclude(key=user.key):
    print(client_user.nickname)

시간을 변경해야하는 로직이 있을경우 freezegun을 이용하자

freezegun이란

python 테스트가 datetime 모듈을 통해 시간을 여행할 수 있게 도와주는 라이브러리 입니다.

링크 : https://github.com/spulec/freezegun

테스트 코드 작성 예시

테스트 코드를 작성하다보면 시간의 예외처리 또는 시간을 이용한 데이터를 처리해야 하는 경우가 있습니다. ex) 밀리그램 기능에서 어제, 2일전,3일전 등등등 식단 데이터를 올릴경우에 대한 데이터 처리

그래서 저는 python에서 freezegun이라는 라이브러리를 통해 테스트 코드를 작성했습니다. 먼저 코드 예시는 아래와 같습니다.

이 코드는 5일연속 데이터를 잘 올리는지에 대한 테스트 코드 예시입니다. 여기서 필요한 것은 5일간의 날짜 데이터인데 freezegun에서 freeze_time을 import해서 진행할 수 있습니다.

from freezegun import freeze_time

def test_check_5_combo(self):
        fake = Faker()
        user = User.objects.get(nickname="test_nickname")
        datetimes = (datetime.datetime(2021, 11, day) for day in range(26, 31))
        for now_date in datetimes:
            with freeze_time(now_date):
                # print(timezone.now())
                id = fake.word()
                data = {
                    "data": [
                        {
                            "date": int(invert_timestamp(timezone.now())),
														"기타" : "등등",
														"데이터가" : "여러가지",
														"있습니다" : "각자",
														"알맞은" : "데이터넣어서진행",
                        }
                    ],
                }
                self.client.patch(self.url, data=data, format="json")

여기서 with freeze_time(now_date)를 보면 정해진 시간으로 freeze할 수 있는 것을 확인할 수 있습니다.

작성한 테스트코드가 잘 동작하는지 코드 커버리지 확인하기

코드 커버리지란

소프트웨어의 테스트를 논할 때 얼마나 테스트가 충분한가를 나타내는 지표중 하나입니다. 말그대로 코드가 얼마나 커버가 되었나?.. 라는 뜻을 가지고 있습니다. 소프트웨어 테스트를 진행했을 때 코드 자체가 얼마다 실행되었는지 확인하는 척도입니다.

coverage사용하기

coverage를 사용하기 위한 세팅

pip install coverage

리포트를 보기 전에 run을 먼저 실행해야 합니다.

coverage run manage.py test

그 후 report를 보면 어느정도 테스트코드가 coverage되었는지 확인할 수 있습니다.

coverage report

다음과 같이 .py파일 별로 어느정도의 커버리지가 커버 되었는가 확인할 수 있습니다. 어떤 코드가 커버리지가 안된거지 확인하고 싶으면 아래 명령어를 통해 html 파일을 생성하겠습니다.

 

coverage html

  • 이런식으로 커버를 못한 부분은 빨간색 처리가 되어 있습니다.
  • 커버가 된 곳은 초록색으로 처리가 되어 있습니다.
  • 이것을 참고하면서 어떤 부분이 커버를 못했는가 확인하고 테스트 코드를 작성하면 더욱 좋습니다.
  • 너무 100%되도록 집작하지말고 최소한 80퍼 이상은 커버하자!라는 생각을 가지고 테스트 코드를 작성하면 되겠습니다.

참고할만한 링크

DRF-testing : https://www.django-rest-framework.org/api-guide/testing/

Django-test : https://developer.mozilla.org/ko/docs/Learn/Server-side/Django/Testing

python-unittest : https://docs.python.org/ko/3/library/unittest.html

python-coverage : https://coverage.readthedocs.io/en/6.2/

'Django > Django REST framework' 카테고리의 다른 글

Sendbird를 이용한 DRF 채팅서버 구현  (0) 2022.06.24
Django FCM 개발(DRF)  (0) 2022.03.01
14-Authentication  (0) 2021.09.26
13-Testing  (0) 2021.08.24
12-Validators  (0) 2021.08.18

댓글