이번에 새로운 프로젝트를 진행하게 되었다.
프로젝트 기획 회의 중 이번에는 테스트코드를 제대로 활용하는 방안으로 가는 것으로 이야기가 나왔다.
테스트코드에 대해서 깊게 알지 못했던 나는 이번 기회를 통해 테스트코드 즉 TDD에 대해 살펴보고 연습해보기로 했다.
1. TDD란 ?
테스트 주도 개발(TDD, Test-Driven Development)
테스트를 먼저 작성한 후 그 테스트를 통과하기 위한 최소한의 코드를 작성하고, 이후에 리팩토링을 통해 코드를 개선하는 개발 방법론이다.
* TDD의 세 가지 주요 단계
1) Red (실패하는 테스트 작성)
• 원하는 기능에 대한 테스트를 작성하지만, 아직 기능이 구현되지 않았기 때문에 테스트가 실패
• 예: 특정 URL로 요청하면 “Hello, world!“를 반환해야 한다는 테스트 작성
2) Green (테스트 통과)
• 테스트를 통과하기 위해 최소한의 코드를 작성
• 코드의 품질보다는 테스트 통과가 목표
3) Refactor (리팩토링)
• 테스트가 통과한 후, 코드를 더 깔끔하고 효율적으로 리팩토링
• 리팩토링 후에도 테스트가 여전히 통과해야함
본인은 보통 어떠한 기능을 개발한다고 하면 바로 실행에 옮겨서 코드를 작성했다.
코드를 작성하는 과정에서 코드가 잘 작성되고 있는지 중간 중간에 print 메소드를 통해 과정을 확인했다.
코드 즉 api를 작성 한 후에는 POSTMAN 등을 통해 테스트를 하였다.
그러나 TDD 방법론은 이와 반대의 개념이다.
테스트 코드를 먼저 작성하고 그에 따라 테스트에 통과되도록 코드를 작성한다.
작성한 코드가 통과 되었다면 리팩토링을 통해 코드 품질을 개선한다.
2. TDD가 필요한 이유
- 명확한 개발 방향 제공
최초 테스트코드를 작성할 때 본인이 작성하려는 api가 어떤 기능을 제공해야하는지 생각하고 작성하게 된다.
이를 통해 좀 더 명확한 개발 방향을 가지고 코드를 작성할 수 있다. - 품질 높은 코드 작성
테스트 코드를 통과하기 위해 최소한의 코드를 작성하고 리팩토링을 통해 코드를 개선한다.
이러한 과정을 무작정 코드를 작성하는 것 보다 품질 높은 코드를 제공한다. - 변화와 유지보수 용이
요구사항 변경이나 리팩토링이 필요할 때 기존 테스트를 통해 코드의 변경이 기존 동작을 깨뜨리지 않았음을 확인할 수 있다.
이는 코드의 안정성과 유지보수성을 높여준다. - 개발 속도 향상
초기에는 일일히 테스트를 작성하는것이 느려 보이지만, 버그를 초기에 발견하고 수정하고,
또한 기존 테스트를 지속적으로 재활용함으로써 전체 개발 사이클 속도를 높인다.
특히 디버깅에 소요되는 시간을 줄이는데 효과적이다.
3. Django의 TDD
우리의 만능 프레임워크 Django는 TDD를 쉽게 구현할 수 있도록 강력한 테스트 프레임워크를 제공한다.
특히 Python 표준 라이브러리인 unittest 모듈을 기반으로 확장된 Django의 내부 모듈들에 의해 실행된다.
참고로 Django는 테스트코드를 실행할 때 따로 DB를 생성해서 테스트 한다.
즉 우리가 사용하는 DB에 영향을 주지 않는다는 점이다.
보통 우리가 만든 app을 보면 tests.py가 자동으로 생성된다.
이 파일에 테스트 코드를 작성해도 된다.
혹은 test~.py로 시작하는 형태의 파일을 모두 테스트의 대상이 된다.
Django에서 테스트코드를 실행하는 방식은 간단하다.
# 기본 실행 방법
python manage.py test
# 특정 앱의 테스트만 실행
python manage.py test <app_name>
# 특정 테스트 모듈 실행
python manage.py test <app_name>.<module_name>
# 특정 테스트 클래스 실행
python manage.py test <app_name>.<module_name>.<TestClass>
# 특정 테스트 메소드 실행
python manage.py test <app_name>.<module_name>.<TestClass>.<test_method>
이렇듯 본인이 원하는 단위대로 테스트를 할 수 있다.
다음은 편리한 옵션에 대해 살펴보겠다.
# verbosity 설정
# 테스트 출력의 상세 정도를 설정할 수 있다.
# 0: 최소 출력 / 1: 기본 출력(기본값) / 2: 상세 출력
python manage.py test --verbosity <level>
# 데이터베이스 재사용
# 테스트를 반복 실행할 때 데이터베이스 생성 시간을 절약하려면 기존 테스트 데이터베이스를 재사용할 수 있음
python manage.py test --keepdb
# 특정 패턴을 사용하여 테스트 파일 지정
python manage.py test --pattern <pattern>
# 모든 옵션을 확인하려면 아래와 같이 실행
python manage.py test --help
# 예) 여러 옵션을 조합한 실행
python manage.py test myapp --parallel 4 --verbosity 2 --keepdb
이제 기본적인 TDD에 대해서는 알아보았다.
그래도 당최 이렇게만 봐서는 모르겠으니 아주 간단한 예제로 실습을 해보겠다.
4. TDD 실습 ( DRF )
해당 실습은 간단한 이해를 돕기 위해 매우 쉬운 예제로 진행하겠다.
우선 어떤 기능을 만들지 먼저 생각해야한다.
본인은 유저의 id로 get 요청을 했을 때 상세정보를 반환하는 기능을 만들것이다.
우선 아무것도 작성하지 않은채로 테스트를 진행해보겠다.
python3 manage.py test
테스트과 통과한 모습이다. 통과했다기 보다 테스트할게 없으니 통과된것이다.
이제 tests.py에 테스트코드를 작성한다.
1) 테스트코드 작성 (실패를 위한)
from rest_framework.test import APITestCase
from rest_framework import status
from user.models import User
from django.urls import reverse
class UserAPITest(APITestCase):
def test_get_user(self):
"""
유저 상세 정보를 조회할 수 있어야 한다.
"""
# test 객체 생성
user_instance = User.objects.create(
email='test@test.com',
name='test_user'
)
url = reverse('get_user_detail', kwargs={'id': user_instance.id})
# API 호출
response = self.client.get(url)
# response status code 확인
self.assertEqual(response.status_code, status.HTTP_200_OK, '200 code가 아니다.')
# response 데이터에서 email 값 확인
self.assertEqual(response.data['email'], 'test@test.com', '이메일이 일치하지 않는다.')
# response 데이터에서 name 값 확인
self.assertEqual(response.data['name'], 'test_user', '이름이 일치하지 않는다.')
# created_at 값이 None이 아니어야 한다
self.assertIsNotNone(response.data['created_at'], '가입날짜가 None이다.')
APITestCase는 DRF에서 제공하는 테스트 클래스이다.
HTTP 요청(POST, GET, PUT, DELETE 등)을 쉽게 테스트할 수 있도록 클라이언트 객체를 제공한다.
assertEqual(a, b, msg=None) : 두 값이 동일한지 비교하고, 동일하지 않을 경우 테스트를 실패 처리
- a : 비교할 첫번째 값
- b : 비교할 두번째 값
- msg : 테스트 실패시 출력할 메시지 (선택)
assertIsNotNone(expr, msg=None) : expr 값이 None이 아닌지 확인하는데 사용
- expr : 확인할 값 (표현식, 변수, 함수 결과 등)
- msg : 테스트 실패시 출력할 메시지 (선택)
이제 작성된 테스트 코드를 실행해본다.
테스트코드가 실패되면 성공이다.
당연히 우리는 아무런 api, serializer, urls를 작성하지 않았다. 오류가 발생하는 것이 당연하다.
python3 manage.py test
정상적으로 오류가 발생했다면 이제 테스가 오류가 나지 않고 통과하기 위한 최소한의 코드를 작성한다.
2) 코드 작성 (통과를 위한)
# serializers.py
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ['id', 'email', 'name', 'is_active', 'is_staff', 'created_at']
# apis.py
@api_view(['GET'])
@permission_classes([AllowAny])
def get_user_detail(request, id):
user = User.objects.get(pk=id)
serializer = UserSerializer(user)
return Response(serializer.data, status=status.HTTP_200_OK)
# urls.py
urlpatterns = [
path('api/users/<id>', get_user_detail, name='get_user_detail'),
]
해당 테스트를 통과하기 위한 최소한의 코드를 작성했다.
이제 다시 테스트를 진행해보겠다.
3) 재테스트
python3 manage.py test
정상적으로 테스트가 통과되었음을 확인할 수 있다.
자세히 보면 얼마나 시간이 소요되었는지도 확인할 수 있다.
테스트를 통해 코드를 작성해가며 품질을 향상시킬 수 있다는 뜻이다.
한가지 더 테스트를 해보겠다.
만약 serializer에 name을 반환하지 않는다면 어떻게 되는지 체크해보겠다.
# serializers.py
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ['id', 'email', 'is_active', 'is_staff', 'created_at']
python3 manage.py test
자세히 확인해보면 어떤 이유로 어떤 부분에서 오류가 발생하여 테스트 에러가 발생했는지 명시된다.
이를 통해 정확한 디버깅과 오류없이 안전성이 보장되는 코드를 작성할 수 있다.
오늘은 TDD에 대해서 알아보고 실습을 해보았다.
본인도 아직 프로젝트를 진행하며 TDD를 제대로 다뤄본적이 없다.
이론적으로는 잘 이해가 가지만 아직 개념을 익힌것에 불과하기 때문에 프로젝트를 진행해봐야 더 자세히 알 것 같다.
이번 프로젝트를 잘 진행 한 후 추후에 TDD 심화에 대해 이야기 해보는 게시물을 작성하도록 하겠다.
'Framework > Django & DRF' 카테고리의 다른 글
[Django] html에 static 파일 적용시키기 (css, js 등) (1) | 2025.01.27 |
---|---|
[Django] 국세청 사업자등록정보 진위확인 및 상태조회 API 사용법 ( + Postman) (0) | 2025.01.15 |
[Django & React] CORS의 모든 것 (5분 안에 해결하기) (0) | 2025.01.02 |
[Django] 초심자에게 발생하는 에러 : didn't return an HttpResponse object (0) | 2023.02.26 |
[Django] table 생성 시 Value Error (1) | 2023.02.26 |