Django Rest Framework
DRF testing
사용자 정보 가져오기 테스트 코드
class LoginUserTest(APITestCase):
def setUp(self):
self.data = {'username': 'user10', 'password': '1010'}
self.user = User.objects.create_user('user10', '1010')
def test_login(self):
response = self.client.post(reverse('token_obtain_pair'), self.data)
self.assertEqual(response.status_code, 200 )
def test_get_user_data(self):
access_token = self.client.post(reverse('token_obtain_pair'), self.data).data['access']
response = self.client.get(
reverse('user_view'),
HTTP_AUTHORIZATION = f'Bearer {access_token}'
)
self.assertEqual(response.data['username'], self.data['username'])
- 로그인 테스트에 이어 회원정보 조회에 대한 테스트도 진행하였다.
- 로그인에 관한 url인
token_obtain_pair
를 통해 client에 post 요청을 보내고(이를 위해 필요한 데이터는 위에 존재), 그 정보들 중에서 access
토큰 값을 가져와 access_token
변수에 넣는다.
- 그 다음으로
user_view
에 get 요청을 보내는데, 이에 필요한 값인 토큰값을 HTTP_AUTHORIZATION
에 담아서 보내준다. 알다시피 user_view
의 get 요청은 회원정보 조회이고, 회원정보를 조회하기 위해서 로그인한 사용자라는 증거인 토큰이 필요했다. 이것을 위와 같이 표현한 것이다.
- 그리고
asserEqual
함수를 사용해 우리가 response
를 보내 얻은 username과 setUp
에서 저장되어 로그인한 사용자의 username이 같은지 비교하는 것이다.
- 결과는 아래와 같다.
- 코드에서 볼 수 있듯이, test.py는 각 app마다 존재할 수 있다. 테스트를 진행할 때
내가 테스트하고 싶은 app을 골라 진행
할 수 있다.
setUpTestData()
- 우리는 지금까지
setUp()
을 활용해 각 테스트 클래스 안에서는 한 번 만들면 테스트가 끝날 때까지 데이터가 유지되도록 하면서 테스트를 진행했었다.
- 그런데 user 부분 말고 article과 같은 것들의 테스트를 진행하게 되면서 쭉 유지가 됐으면 하는 데이터와 할 때마다 갱신하고 싶은 데이터가 있을 수 있다.
- 따라서
setUpTestData()
를 사용해 테스트를 진행해보도록 하자.
setUp()
vs. setUpTestData()
setUp()
: test(method 마다)를 할때마다 실행
되어 test 데이터셋을 만들어준다.
setUpTestData()
: TestCase 한 개마다(class 마다) 실행
되어 초기 데이터 test 데이터를 만들어준다.
class method
from datetime import date
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
@classmethod
def fromBirthYear(cls, name, birthYear):
return cls(name, date.today().year - birthYear)
def display(self):
print(f"{self.name}'s age is {self.age}")
person = Person('James', 29)
person.display()
person1 = Person.fromBirthYear('Micheal', 1995)
person1.display()
- 위의 코드를 보면
@classmethod
를 사용한 부분이 있다. 해당 코드에 대해 알아보도록 하겠다.
- 원래 class는
person
과 같이 인스턴스를 생성해서 해당 클래스 안에 설정된 함수를 사용할 수 있다.
- 하지만 우리가
@classmethod
를 사용하게 되면 person1
과 같이 바로 인스턴스를 거치지 않고 클래스 안의 함수를 사용할 수 있게 된다.
@classmethod
가 다른 함수와 다른 점은 첫 번째 인자로 self가 아닌 cls
, 즉 본인의 class를 가진다는 것이다. 이로 인해 자신의 class를 다시 한 번 실행시켜 줄 수 있고, 이는 우리가 인스턴스를 생성하는 것과 똑같이 실행된다.
- 아래의 순서로 진행되게 된다.
Person.fromBirthYear(cls, name, birthYear)
fromBirthYear(Person, 'Micheal', date.today().year - 1995)
person1 = Person('Micheal', 27)
person1.display()
Micheal's age is 27
static method
from datetime import date
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
@classmethod
def fromBirthYear(cls, name, birthYear):
return cls(name, date.today().year - birthYear)
@staticmethod
def isAdult(age):
return age > 18
person1 = Person('James', 29)
person2 = Person.fromBirthYear('Micheal', 1995)
print(person1.age)
print(person2.age)
print(Person.isAdult(22))
- 위의 코드를 통해 알 수 있듯이
static method
는 self와 cls
가 인자값으로 들어가지 않는다.
@staticmethod
또한 @classmethod
와 같이 인스턴스를 따로 생성해 사용하지 않는 것을 볼 수 있다.
- 다른 점으로는 결과값으로
boolean
값을 준다는 것이 있다.
- 따라서 맨 아래 있는 print문의 결과값은 나이 input 값으로 들어간 22가 18보다 크기 때문에
True
라고 나오게 된다.
- 그렇다면 위의 방식과
바깥에 함수를 만들어서 하는 것
이랑 무슨 차이가 있는것일까?
- 아무런 차이가 없다.
코드의 깔끔함
을 위해서 위의 방식으로 작성하는 것이다.
- 해당 클래스 안에
클래스의 기능성(utility)를 위해서 작성
하게 되는 것이다. 또한 이 클래스 안에서 작동되면 좋을 것 같은 함수
를 위와 같이 작성하게 된다.
아티클 생성 테스트 코드
- 위에서 알아본
@classmethod
를 사용해 작성하도록 하겠다.
from django.urls import reverse
from rest_framework.test import APITestCase
from rest_framework import status
from user.models import User
class ArticleCreateTest(APITestCase):
@classmethod
def setUpTestData(cls):
cls.user_data = {'username': 'user10', 'password': '1010'}
cls.article_data = {'title' : 'test title', 'content' : 'test content'}
cls.user = User.objects.create_user('user10', '1010')
def setUp(self):
self.access_token = self.client.post(reverse('token_obtain_pair'), self.user_data).data['access']
setUpTestData()
를 통해 데이터를 생성하였다.
- access_token은
setUp()
을 사용한 이유는 .client
는 cls와 같이 사용할 수 없기 때문이다.
- 현재는 데이터를 별로 생성하지 않아 굳이 사용하지 않아도 되지 않을까라고 생각할 수 있다. 하지만 나중에 더미 데이터를 수십 수백개 만들게 된다면
TestCase를 시작할 때 한 번
생성한 데이터를 쭉 사용하는 것과 TestCase 안에 있는 각 method를 실행할 때마다
데이터를 생성하는 것에는 시간 차이가 많이 벌어지게 될 것이다.
로그인을 하지 않았을 경우 게시글 작성 테스트
- 테스트 코드를 작성하면서 항상 맨 앞에
test_
를 붙여주는데, 이 부분이 있어야 python manage.py test {app이름}
코드로 테스트를 할 때 실행이 되게 된다. 파일명도 tests.py
이어야 테스트가 진행된다.
- 물론 그냥 일반 method를 생성해 테스트 코드 안에서 사용하는 것도 가능하다.
from django.urls import reverse
from rest_framework.test import APITestCase
from rest_framework import status
from user.models import User
class ArticleCreateTest(APITestCase):
@classmethod
def setUpTestData(cls):
cls.user_data = {'username': 'user10', 'password': '1010'}
cls.article_data = {'title' : 'test title', 'content' : 'test content'}
cls.user = User.objects.create_user('user10', '1010')
def setUp(self):
self.access_token = self.client.post(reverse('token_obtain_pair'), self.user_data).data['access']
def test_fail_if_not_logged_in(self):
url = reverse('article_view')
response = self.client.post(url, self.article_data)
self.assertEqual(response.status_code, 401)
- 위의 코드를 실행한 결과이다.
- 현재 user app까지 총 4개의 테스트 코드가 있지만,
app의 이름을 붙여주어
해당 테스트 코드만 돌아간 것을 알 수 있다.
게시글 작성 테스트(이미지 x)
from django.urls import reverse
from rest_framework.test import APITestCase
from rest_framework import status
from user.models import User
class ArticleCreateTest(APITestCase):
@classmethod
def setUpTestData(cls):
cls.user_data = {'username': 'user10', 'password': '1010'}
cls.article_data = {'title' : 'test title', 'content' : 'test content'}
cls.user = User.objects.create_user('user10', '1010')
def setUp(self):
self.access_token = self.client.post(reverse('token_obtain_pair'), self.user_data).data['access']
def test_create_article(self):
response = self.client.post(
path=reverse('article_view'),
data=self.article_data,
HTTP_AUTHORIZATION=f"Bearer {self.access_token}"
)
self.assertEqual(response.data["msg"], '글 작성 완료')
self.assertEqual(response.status_code, 200)
- 이전에는 args의 형태로 테스트하였다면, 이번에는 kwargs의 형태로 테스트하였다. 둘 다 사용 가능하다.
- assertEqual이 쓰인 것을 보면 좀 더 세세하게 msg의 값을 비교할 수도 있고, 그냥 status_code 값을 비교할 수도 있다.
- 이 때 views.py에 msg가 있어야 한다.
게시글 업로드(이미지 o)
- 테스트 코드를 작성하면서 이미지 파일을 임시로 만들어 주었다.
from django.test.client import MULTIPART_CONTENT, encode_multipart, BOUNDARY
from PIL import Image
import tempfile
def get_temporary_image(temp_file):
size = (200, 200)
color = (255, 0, 0, 0)
image = Image.new('RGBA', size, color)
image.save(temp_file, 'png')
return temp_file
class ArticleCreateTest(APITestCase):
@classmethod
def setUpTestData(cls):
cls.user_data = {'username': 'user10', 'password': '1010'}
cls.article_data = {'title' : 'test title', 'content' : 'test content'}
cls.user = User.objects.create_user('user10', '1010')
def setUp(self):
self.access_token = self.client.post(reverse('token_obtain_pair'), self.user_data).data['access']
def test_create_article_with_image(self):
temp_file = tempfile.NamedTemporaryFile()
temp_file.name = 'image.png'
image_file = get_temporary_image(temp_file)
image_file.seek(0)
self.article_data["image"] = image_file
response = self.client.post(
path=reverse('article_view'),
data=encode_multipart(data=self.article_data, boundary=BOUNDARY),
content_type=MULTIPART_CONTENT,
HTTP_AUTHORIZATION=f'Bearer {self.access_token}'
)
self.assertEqual(response.data["msg"], '글 작성 완료')
- 이미지가 없는 게시글 작성과 똑같은 방식이며, 이미지를 넣었기 때문에 이에 대한 처리를 해준 것이 다른 점이다.
- 이미지를 처리하는 다른 방식도 있을 수 있다.