백엔드 Back-end/장고 Django

Q. 장고Django에서 테스트 코드 작성 시 개발팀 내 관습convention으로 정해야 할 사항들은?

Tap to restart 2023. 5. 21. 21:00
반응형

A. 테스트 팩키지, 테스트 파일의 위치, 테스트 파일명, 테스트 클래스명, 테스트 메소드명, 테스트 함수명, 테스트 코드 작성 방식 등을 미리 정해두는 게 좋다. 

 
미리 정하지 않으면 각자 방식대로 하게 되고, 추후 유지보수가 어렵다. 처음 시작할 때 각자 테스트를 해보고 의견을 취합해서 정하는 것이 좋다. 한번 정하면 바꾸기 쉽지 않고, 기존 테스트 코드가 있기 때문에 관성에 따라 정한 방식대로 작성하게 되기 때문이다.
 

테스트 팩키지

pytest: 파이썬 계열 대표 테스트 팩키지. pytest 없이도 django에서 기본 제공하는 django.test로 테스트 가능하다. pytest를 사용할지 말지 논의가 필요하다.
pytest-django: pytest를 활용하면서 장고django 테스트할 때 쓰는 팩키지
 
factory_boy: 테스트 고정값fixture를 쉽게 만들 수 있도록 도와주는 팩키지. 장고 모델 바탕으로 바로 만들어줘서 유용하다. 
model_bakery: 장고에서 테스트 고정값을 쉽게 만들 수 있도록 도와주는 팩키지. factory_boy와 비슷하다. 
 
내 경우 pytest, pytest-django, factory_boy를 같이 쓰고 있다.
 

테스트 파일의 위치

장고에서 python manage.py startapp app으로 app을 추가하면 해당 앱 안에 tests.py란 파일도 같이 추가된다.

또는 자바처럼 src 디렉터리 구조를 그대로 본 따 test 밑에 디렉터리 구조를 만들 수도 있다.

자바 테스트 디렉터리 구조

이때도 tests로 할지 test로 할지 정해야 한다.
내 경우는 소스코드와 테스트를 같은 디렉터리에 두는 것보다 디렉터리 구조를 갖게 해서 자바처럼 따로 두는 것을 선호한다. 
 
fastapi를 만든 tiangolo의 fastapi 예제 프로젝트를 보면 위 자바 예와 비슷하되 test 파일이 모여 있는 디렉터리명은 tests인 것을 확인할 수 있다. 

 

테스트 파일명

장고의 테스트 파일명은 tests.py다. 자바의 경우 DemoApplication.java의 테스트 파일은 DemoApplicaionTest.java이다.
fastapi 예제 프로젝트에서는 test_users.py로 api_v1/endpoinds/ 디렉터리 안에 있는 파일명 앞에 test를 붙인 형태다.

이런 구성인 이유는 pytest가 파일명 앞에 test_를 붙이는 형태를 권장하기 때문이다. 

출처: Tests outside application code
 
내가 선호하는 방식은 src 디렉터리 그대로 본 따고 test_ 를 파일명 앞에 붙이는 형태다. 디렉터리를 자바처럼 그대로 본 따야 헷갈리지 않기 때문이다. 
backend/
    apis/
        users.py
    tests/
        apis/
            test_users.py
 

테스트 작성 단위와 테스트 함수명, 테스트 클래스명, 테스트 메소드명

테스트 작성 단위도 고민해야 한다. 객체 지향 언어인 Java의 경우 class 없이 함수만 작성할 수 없어서 이런 부분을 고민할 필요가 없다. 파이썬은 함수도 가능하고, 클래스도 가능하므로 고민이 필요하다. 함수 단위로 할지, 클래스 단위로 할지 말이다. 
 
장고의 경우 테스트 클래스를 만들고, 그 안에 테스트 메소드를 작성한다.
아래 예의 경우 Entry model 클래스에 대한 테스트 클래스로 EntryTestCast란 클래스명으로 test_manager_active란 테스트 메소드명으로 테스트 코드가 작성된 것을 볼 수 있다.
 

 

내 경우 테스트를 함수보다 클래스 단위로 묶는 것을 선호한다. 파이썬 함수에 대한 테스트를 작성할 때도 함수는 1개지만 테스트 케이스는 여러 개인 경우가 많기 때문이다. DRF(Django REST Framework) ViewSet 클래스에 대한 테스트 작성 시 ViewSet명 앞에 Test를 붙인 형태로 작성한다. 예를 들면 FormViewSet 클래스의 테스트 클래스명은 TestFormViewSet이다.
 
테스트 함수명 또는 테스트 메소드명은 오픈소스를 찾아보면 영어로 설명식으로 작성하는 경우가 많다. FastAPI 예제 프로젝트를 보면 아래처럼 작성했다.

def test_get_users_superuser_me()
def test_get_users_normal_user_me()
def test_create_user_new_email()
def test_create_user_existing_username()
def test_create_user_by_normal_user()
def test_retrieve_users()

 
내 경우 API 엔드포인트별 테스트 코드를 작성한다면 test_{action명}_{user}_{상태 코드} 를 기본으로 작성했다. action명은 list, create, retrieve, update, partial_update, delete 등을 말한다. user는 anonymous, staff 등이다. 상태코드는 http 응답 상태 코드다. 예를 들면 test_create_anonymous_403, test_list_staff_200 등으로 말이다.
 

테스트 작성 방식

pytest를 사용하지 않고 Django가 제공해주는 TestCase를 활용해서 작성할 수도 있다. 또는 API 테스트라면 DRF(Django REST Framework)에서 장고 TestCase를 API 테스트용으로 확장한 APITestCase를 활용해서 작성할 수도 있다. 또는 pytest-django 팩키지를 활용해서 테스트를 작성할 수도 있다. 어떤 방식이든 합의를 하는 게 필요하다. 내 경우 주로 DRF의 APIClient와 pytest-django를 주로 활용해서 작성했다. 
 

from django.test import TestCase
from myapp.models import Animal


class AnimalTestCase(TestCase):
    def setUp(self):
        Animal.objects.create(name="lion", sound="roar")
        Animal.objects.create(name="cat", sound="meow")

    def test_animals_can_speak(self):
        """Animals that can speak are correctly identified"""
        lion = Animal.objects.get(name="lion")
        cat = Animal.objects.get(name="cat")
        self.assertEqual(lion.speak(), 'The lion says "roar"')
        self.assertEqual(cat.speak(), 'The cat says "meow"')

출처: Django Documentation: Writing tests
왜 setUp은 카멜케이스인지는 모르겠다. 
 
내 경우는 아래처럼 주로 작성했다.

@pytest.mark.urls(urls="apis.v1.urls")
@pytest.mark.django_db
class TestFormViewSet:
    VIEW_LIST = "form-list"
    VIEW_DETAIL = "form-detail"
    VIEW_SUBMIT = "form-submit"

    def test_create_anonymous_403(self, client_anonymous):
        path = reverse(viewname=self.VIEW_LIST)
        response = client_anonymous.post(path=path, data={})

        assert response.status_code == status.HTTP_403_FORBIDDEN

출처: forms 프로젝트
 

테스트 작성 시 주석

장고의 경우 아래 예처럼 테스트 메소드 밑에 주석Docstrings을 추가한다. 

class EventTestCase(DateTimeMixin, TestCase):
    def test_manager_past_future(self):
        """
        Make sure that the Event manager's `past` and `future` methods works
        """
        Event.objects.create(date=self.yesterday, pub_date=self.now, headline="past")
        Event.objects.create(date=self.tomorrow, pub_date=self.now, headline="future")

        self.assertQuerysetEqual(
            Event.objects.future(), ["future"], transform=lambda event: event.headline
        )
        self.assertQuerysetEqual(
            Event.objects.past(), ["past"], transform=lambda event: event.headline
        )

출처: djangoproject.com
 
특별히 주석을 추가하지 않는 예도 많다. 어떤 식으로 작성할지 논의가 필요하다.
 

반응형