제가 구현한 간단한 Test Case 전체 코드는 GitHub에서 보실 수 있습니다.
나는 지금까지 개인 프로젝트 구현시 테스트 코드에 대해서 딱히 중요하게 생각하지 않아 테스트 코드를 작성하지 않고 개발을 진행했다.
하지만 최근에 APIView로 구현된 뷰에서 중복된 코드가 너무 많아 중복된 코드를 줄이고 간결하게 작성하고 싶어 Generic으로 변경하였다.
내가 원하는 건 APIView로 구현된 기능이 변경되지 않고 Generic으로 바꾸는 것인데, CRUD 작업을 하나하나 바꿀 때마다 PostMan에서 일일이 확인하는게 너무 오래걸렸고, 이 작업을 간결화 할 수 있는 Django의 Test Case의 중요성이 생각나서 앞으로 프로젝트 구현시 TDD 방식으로 구현하지 않더라도 항상 Test Case는 작성해야겠다는 다짐을 했다.
Test Case를 통해 리팩토링이나 새로운 기능 추가시 이전의 구현된 기능이 정상적으로 작동하는지 빠르게 확인할 수 있도록 하자!
TestCase는 장고에서 기본으로 제공하는 테스트 기능으로, 코드 변경으로 인해 발생하는 오류를 방지하기 위해 사용한다.
App 디렉토리 생성시 default로 같이 생성되는 tests.py
파일이 테스트 기능을 위한 파일이다.
DRF의 APITestCase는 Django의 기본 TestCase를 상속하여 API 테스트를 위한 확장 기능을 가진 클래스이다.
즉, API 테스트 시에 format
옵션을 통해 Content-Type을 쉽게 조절할 수 있어 DRF의 APITestcase를 사용하는 것이 좋다.
@classmethod
데코레이터를 붙여 클래스 수준의 데이터로 설정.즉, setUpTestData 메소드는 클래스 내 테스트 함수에서 공통적으로 사용할 데이터를
setUp 메소드는 각 테스트 함수마다 독립적으로 사용될 데이터를 정의한다.
# Example : 게시글 CREATE TEST
class PostCreateTestCase(APITestCase):
@classmethod
def setUpTestData(cls):
# cls는 self와 같은 역할이며, class 단위의 메소드에서 사용된다.
cls.user = User.objects.create_user(
username= "kimjihong",
password= "password",
email= "kinjihong9598@gmail.com",
fullname= "kimjihong"
)
def setUp(self):
self.data = {
"title": "제목",
"contents": "내용",
"owner": self.user.pk
}
def test_posts_create_without_required_field(self):
self.data.pop("title") # 필수 필드 pop
self.client.post(
path= "end-point",
data= self.data,
format= 'json'
)
# ... 테스트 로직 ...
위와 같이 만약, 필수 필드에 대한 test case 에서
데이터를 pop 하는 과정을 진행한다면 해당 데이터는 setUp 함수에 정의해서 각 테스트 함수마다 독립적으로 정의하는게 좋고
클래스 내 모든 테스트 함수에서 공통적으로 사용될 User instance 같은 데이터는 setUpTestData 함수에 한번만 정의하는게 좋다.
Django의 TestCase와 달리 APITestCase에서는 credentials
함수로 간편하게 client의 Authorization 헤더에 JWT access 토큰을 넣을 수 있다.
class PostCreateTestCase(APITestCase):
@classmethod
def setUpTestData(cls):
refresh_token = RefreshToken.for_user(user= self.user)
access_token = refresh_token.access_token
self.client.credentials(HTTP_AUTHORIZATION= f'Bearer {access_token}')
# ...
만약 Authorization 헤더에 등록된 토큰을 없애려면 self.client.credentials()
라고 호출해서 초기화 할 수 있다.
class PostCreateTestCase(APITestCase):
# ...
def test_post_with_unauthorized(self):
# authorization 헤더 초기화 하기
self.client.credentials()
나는 JWT 토큰 발급시 HttpOnly 속성으로 지정하고
MiddleWare에서 client의 Cookie에 저장된 access 토큰 정보를 authorization 헤더로 옮기도록 구현했다.
그래서 나는 JWTSetupMixin
라는 Mixin 클래스를 정의하고, 상속하여 인증이 필요한 테스트 함수에서 client의 쿠키에 Token을 저장하는 방식으로 사용했다.
# boards/test/common.py
class JWTSetupMixin:
def api_authentication(self, client, user):
# refresh, access token 발급
refresh_token = RefreshToken.for_user(user= user)
access_token = refresh_token.access_token
# 미들웨어에서 access 토큰은 authorization 헤더에 추가하도록 custom 해놔서
# 따로 credentials 로 추가 안하고 쿠키내 token 을 포함
cookie = SimpleCookie()
cookie['refresh'] = refresh_token
cookie['access'] = access_token
client.cookies = cookie
# boards/test/test_post.py
class PostCreateTestCase(APITestCase, JWTSetupMixin):
# ...
def test_posts_create_without_required_field(cls):
self.api_authentication(self.client, self.user)
# ...
자세한 코드는 GitHub에서 볼 수 있다.
본격적으로 Test Case를 작성해보자.
Django 에서는 Test Case를 test
라는 키워드로 탐색하기 때문에
테스트를 진행할 함수명은 항상 test_
로 시작해서 테스트 함수라는 것을 알려줘야 한다.
def create_post_success(self):
# test 코드를 작성해도 실행되지 않음.
def test_create_post_success(self):
# test 실행.
Test Case의 실질적인 로직은 아래와 같다.
assert
진행Test Case에서 response 데이터를 확인할 때, 가장 많이 사용하는 가정 설명문은 self.assertEqual
함수이다.
# Example CREATE method
class PostCreateTestCase(APITestCase, JWTSetupMixin):
@classmethod
def setUpTestData(cls):
cls.user = User.objects.create_user(
username= "kimjihong",
password= "password",
email= "kinjihong9598@gmail.com",
fullname= "kimjihong"
)
def setUp(self):
self.data = {
"title": "제목",
"contents": "내용",
"owner": self.user.pk
}
def test_create_post_success(self):
"""
case: 정상적으로 새로운 게시글이 생성될 경우
1. 201 Created 응답.
2. owner 는 요청을 보낸 사용자로 자동 생성 (반환값은 pk가 아닌 username 이어야함.)
"""
# client 에 refresh, access 토큰 설정 (JWTSetupMixin)
self.api_authentication(self.client, self.user)
response = self.client.post(
path= f'{BASE_API_URL}/posts',
data= self.data,
format= 'json'
)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(response.data['owner'], self.user.username)
간단하게 설명하자면, assertEqual
을 통해 응답 코드 확인과 owner 필드값이 정상적으로 반영되었는지 확인했다.
내가 구현한 Test Case는 GitHub에서 확인 가능하다.
Django 또는 DRF에서 테스트 코드를 작성하고 테스트 실행하는 방법은 2가지이다.
# directory structure
.
├── appname
│ ├── test
│ │ ├── __init__.py
│ │ ├── test_file.py
│ │ └── ...
│ ├── __init__.py
│ └── ...
...
# case.1: 전체 Test Case 실행
python3 manage.py test
# case.2: 특정 경로의 Test Case 실행
python3 manage.py test appname
python3 manage.py test appname.test_file
python3 manage.py test appname.test_file.test_class
python3 manage.py test appname.test_file.test_class.test_method
특정 경로의 Test Case 실행 방법에서 경로는 .
구분자로 나타낼 수 있고, 파일의 경로 및 class 또는 class 내 함수까지 경로 지정이 가능하다.
DRF docs - Testing
stackoverflow - what is difference between TestCase and APITestCase