Django DRF Test Code 활용하기
01. 테스트 코드
- Django에서 startapp을 통해 만들어진 앱에는 test.py라는 파일이 자동으로 생성된다. 이 파일에 임의의 테스트코드를 적고 터미널에서 아래와 같이 입력하면 테스트 결과를 출력해준다.
python manage.py test
- 임의로 아래와 같이 test.py에 작성하고 위의 명령어를 통해 test하면 결과를 출력해주는 것을 확인할 수 있다.
# test.py
from django.test import TestCase
# Create your tests here.
class TestView(TestCase):
def test_two_is_three(self):
self.assertEqual(2,3)
def test_two_is_two(self):
self.assertEqual(2,2)
"""
Found 2 test(s).
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
F.
======================================================================
FAIL: test_two_is_three (users.tests.TestView)
----------------------------------------------------------------------
Traceback (most recent call last):
File "...", line 6, in test_two_is_three
self.assertEqual(2,3)
AssertionError: 2 != 3
----------------------------------------------------------------------
Ran 2 tests in 0.002s
FAILED (failures=1)
Destroying test database for alias 'default'...
"""
02. 실습(1)_회원가입에 대한 테스트
- 아래처럼 test.py에 user_data를 url과 함께 client.post를 보내고, status_code가 어떻게 출력되는지 테스트 코드를 작성하여 확인할 수 있다.
# users/test.py
from django.urls import reverse
from rest_framework.test import APITestCase
from rest_framework import status
class UserRegistrationAPIViewTestCase(APITestCase):
def test_registration(self):
url = reverse("user_view") # reverse를 통해 "user_view"에 해당되는 name의 url을 가져온다.
user_data = {
"username" : "testuser",
"fullname" : "테스터",
"email" : "test@testuser.com",
"password" : "password",
}
response = self.client.post(url, user_data) # url, user_data를 담아 client.post를 보내고 response를 받아온다.
self.assertEqual(response.status_code, 200) # status_code가 200인지 확인
"""
Found 1 test(s).
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.
----------------------------------------------------------------------
Ran 1 test in 0.165s
OK
Destroying test database for alias 'default'...
"""
- 에러를 발생시켰을 때도 마찬가지로 확인해볼 수 있다. 이 때 print(response.data)를 아래처럼 추가해주었다.
# users/test.py
from django.urls import reverse
from rest_framework.test import APITestCase
from rest_framework import status
class UserRegistrationAPIViewTestCase(APITestCase):
def test_registration(self):
url = reverse("user_view") # reverse를 통해 "user_view"에 해당되는 name의 url을 가져온다.
user_data = {
"username" : "testuser",
# "fullname" : "테스터",
"email" : "test@testuser.com",
"password" : "password",
}
response = self.client.post(url, user_data) # url, user_data를 담아 client.post를 보내고 response를 받아온다.
print(response.data)
self.assertEqual(response.status_code, 200) # status_code가 200인지 확인
"""
Found 1 test(s).
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
{'message': "${'fullname': [ErrorDetail(string='This field is required.', code='required')]}"}
F
======================================================================
FAIL: test_registration (users.tests.UserRegistrationAPIViewTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
File "...", line 17, in test_registration
self.assertEqual(response.status_code, 200) # status_code가 200인지 확인
AssertionError: 400 != 200
----------------------------------------------------------------------
Ran 1 test in 0.019s
FAILED (failures=1)
Destroying test database for alias 'default'...
"""
03. 실습(2)_로그인에 대한 테스트
- 위에서 회원가입 테스트 코드를 짰으니, 바로 아래에 로그인에 대한 테스트 코드를 짜서 오류가 발생하는지 확인해본다. url만 사용하고 있는 "token_obtain_pair"로 바꾸어주면 된다.
# users/test.py
from django.urls import reverse
from rest_framework.test import APITestCase
from rest_framework import status
class UserRegistrationAPIViewTestCase(APITestCase):
def test_registration(self):
url = reverse("user_view") # reverse를 통해 "user_view"에 해당되는 name의 url을 가져온다.
user_data = {
"username" : "testuser",
"fullname" : "테스터",
"email" : "test@testuser.com",
"password" : "password",
}
response = self.client.post(url, user_data) # url, user_data를 담아 client.post를 보내고 response를 받아온다.
print(response.data)
self.assertEqual(response.status_code, 200) # status_code가 200인지 확인
def test_login(self):
url = reverse("token_obtain_pair")
user_data = {
"username" : "testuser",
"fullname" : "테스터",
"email" : "test@testuser.com",
"password" : "password",
}
response = self.client.post(url, user_data)
print(response.data)
self.assertEqual(response.status_code, 200) # status_code가 200인지 확인
"""
Found 2 test(s).
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
{'detail': ErrorDetail(string='No active account found with the given credentials', code='no_active_account')}
F{'message': '가입 완료!!'}
.
======================================================================
FAIL: test_login (users.tests.UserRegistrationAPIViewTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
File "D:\JAY\sp_prjs\ai2_back\users\tests.py", line 30, in test_login
self.assertEqual(response.status_code, 200) # status_code가 200인지 확인
AssertionError: 401 != 200
----------------------------------------------------------------------
Ran 2 tests in 0.327s
FAILED (failures=1)
Destroying test database for alias 'default'...
"""
- 그런데 이상한 점은 분명 위에서 회원가입을 했는데 login 부분에서 에러가 발생한다. 이는 테스트할 때에는 테스트 db를 사용하고, 하나의 테스트가 끝나면 테스트 db가 초기화 되기 때문이다. (*각 test끼리는 independent이며 stateless하다)
- 이를 방지하는 방법은 setUp이라는 메서드를 사용하는 것이다.(*참고 링크)
아래 샘플 코드에서 볼 수 있듯이 setUp이라는 메서드에서 정의한 것들을 아래 test_animals_can_speak에서 그대로 사용하고 있다. 이렇듯 setUp이라는 이름을 사용하는 메서드에서 사전에 정의해준다면 다른 테스트에서도 사용이 가능한 것이다.
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"')
- 또 다른 문서에서 보면 setUp, 그리고 테스트가 마무리 되었을 때에는 tearDown을 사용할 수 있다고 나와있다. (*참고 링크)
class YourTestClass(TestCase):
def setUp(self):
# Setup run before every test method.
pass
def tearDown(self):
# Clean up run after every test method.
pass
def test_something_that_will_pass(self):
self.assertFalse(False)
def test_something_that_will_fail(self):
self.assertTrue(False)
- 위에서 확인했던 setUp 메서드를 이용해서 로그인 테스트 코드를 다시 작성해보면 정상적으로 작동됨을 확인할 수 있다.
# users/test.py
class LoginUserTest(APITestCase):
def setUp(self):
self.data = {'username':'john', 'password':'johnpassword'}
self.user = User.objects.create_user('john', 'johnpassword') # create_user는 models.py에서 정의해준 함수
def test_login(self):
response = self.client.post(reverse('token_obtain_pair'), self.data)
print(response.data["access"])
self.assertEqual(response.status_code, 200)
"""
Found 1 test(s).
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
가입
eyJ0eXAiOiJKV1QiLCJ... # 토큰값
.
----------------------------------------------------------------------
Ran 1 test in 0.323s
OK
Destroying test database for alias 'default'...
"""
04. 실습(3)_사용자 정보 가져오기 테스트
- 회원가입, 그리고 로그인이 되면, 로그인이 된 사용자의 정보를 가져오려한다.
- 아래 코드를 보면 access_token이라느 곳에 'token_obtain_pair'의 url에서 post를 한 결과를 .data로 가져오고 'access'라는 키를 통해 접근하여 토큰값을 가져오도록 코드를 작성하였다.
- response는 get의 파라미터로 path, HTTP_AUTHORIZATION가 있으며, path에는 "user_view"라는 이름(urls.py에서 name 확인)으로 가져오고, HTTP_AUTHORIZATION은 헤더에 들어갈 내용을 f-string으로 작성하여 넣어 assertEqual을 통해 잘 가져오고 있는지 확인한다.
- 결과 확인은 아래 세 가지로 원하는 유형에 따라 변경하여 사용할 수 있다.
# users/test.py
class LoginUserTest(APITestCase):
...
def test_get_user_data(self):
access_token = self.client.post(reverse('token_obtain_pair'), self.data).data['access']
response = self.client.get(
path=reverse("user_view"),
HTTP_AUTHORIZATION=f"Bearer {access_token}"
)
# print(response.data)
# self.assertEqual(response.status_code, 200)
self.assertEqual(response.data['username'], self.data['username'])
05. setUpTestData
- 그런데 setUp은 테스트 할 때마다 매번 실행되어야 하므로 비효율적이다. 몇 가지 항목은 처음 한번 셋업시 실행되고 계속 값을 가지고 있어도 되는 경우가 있다. 아래를 보면 setUp은 run once for every test mothod이지만, setUpTestData는 Run once to set up non-modified data이다. (*참고 링크)
class YourTestClass(TestCase):
@classmethod
def setUpTestData(cls):
print("setUpTestData: Run once to set up non-modified data for all class methods.")
pass
def setUp(self):
print("setUp: Run once for every test method to setup clean data.")
pass
- 그런데 상단에 @classmethod라는 것이 보인다.
06. @classmethod, @staticmethod
- @classmethod : @classmethod가 상단에 붙은 함수는 클래스로 별도의 인스턴스를 생성하지 않아도 외부에서 해당 함수를 호출하여 사용이 가능하게 한다.
- @staticmethod : 클래스 내부에 정의된 함수를 클래스 밖에서 정의한 것과 동일하게 동작되도록 하는 것으로, 함수 밖에 정의하여도 상관없지만 특정 클래스 내부에 해당 함수를 정의하여 코드를 정리하고 싶을 경우에 사용한다.
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
@classmethod
def fromBirthYear(cls, name, year):
return cls(name, date.today().year-year)
@staticmethod
def isAdult(age):
return age > 18
person1 = Person('mayank', 21)
person2 = Person.fromBirthYear('mayank', 1996)
print(person1.age)
print(person2.age)
# print the result
print(Person.isAdult(22))
07. setUp을 @classmethod로 변환하기
class ArticleCreateTest(APITestCase):
@classmethod
def setUpTestData(cls):
cls.user_data = {'username':'john', 'password':'johnpassword'}
cls.article_data = {"title":"some title", "content":"some content"}
cls.user = User.objects.create_user('john', 'johnpassword')
def setUp(self):
self.access_token = self.client.post(reverse('token_obtain_pair'), self.user_data).data['access']
# def setUp(self):
# self.user_data = {'username':'john', 'password':'johnpassword'}
# self.article_data = {"title":"some title", "content":"some content"}
# self.user = User.objects.create_user('john', 'johnpassword')
# self.access_token = self.client.post(reverse('token_obtain_pair'), self.user_data).data['access']
# 여기에선 self.client가 classmethod가 아니기 때문에 별도의 setUp 함수를 생성해야 한다.
08. 로그인 안한 상태에서 게시글 작성 테스트
class ArticleCreateTest(APITestCase):
@classmethod
def setUpTestData(cls):
cls.user_data = {'username':'john', 'password':'johnpassword'}
cls.article_data = {"title":"some title", "content":"some content"}
cls.user = User.objects.create_user('john', 'johnpassword')
def setUp(self):
self.access_token = self.client.post(reverse('token_obtain_pair'), self.user_data).data['access']
# def setUp(self):
# self.user_data = {'username':'john', 'password':'johnpassword'}
# self.article_data = {"title":"some title", "content":"some content"}
# self.user = User.objects.create_user('john', 'johnpassword')
# self.access_token = self.client.post(reverse('token_obtain_pair'), self.user_data).data['access']
# 여기에선 self.client가 classmethod가 아니기 때문에 별도의 setUp 함수를 생성해야 한다.
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)
09. 이미지가 없는 게시글 작성 테스트
# articles/test.py
from django.test import TestCase
from django.urls import reverse
from rest_framework import status
from rest_framework.test import APITestCase
from users.models import User
...
class ArticleCreateTest(APITestCase):
...
# 게시글 작성
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["message"], "글 작성 완료!")
# self.assertEqual(response.status_code, 200)
10. 이미지를 포함한 게시글 작성 테스트
- get_temporary_image에서는 임시 이미지 파일을 만드는 메서드이다.
# 이미지 업로드(임시파일을 만들어 활용하기)
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):
...
# 이미지를 포함한 게시글 작성 테스트
def test_create_article_with_image(self):
# 임시 이미지 파일 생성
temp_file = tempfile.NamedTemporaryFile() # 임시 파일 만들기
temp_file_name = "image.png" # 이름을 image.png로 지정
image_file = get_temporary_image(temp_file) # 이미지 파일 받아오기
image_file.seek(0) # 이미지의 첫 번째 프레임을 받아오기
self.article_data["image"] = image_file # article_data에 이미지 파일을 넣기
# 전송
response = self.client.post(
path=reverse("article_viiew"),
data=encode_multipart(data = self.article_data, boundary=BOUNDARY),
content_type=MULTIPART_CONTENT,
HTTP_AUTHORIZATION = f"Berer {self.access_token}"
)
self.assertEqual(response.data["message"], "글 작성 완료!")
11. 더미데이터를 위한 Faker 사용
- 더미데이터를 처리하기 위해 Faker라는 라이브러리를 설치해서 사용할 수 있다.(*참고 링크)
pip install Faker
- 참고 1
from faker import Faker
fake = Faker()
fake.name()
# 'Lucy Cechtelar'
fake.address()
# '426 Jordy Lodge
# Cartwrightshire, SC 88120-6700'
fake.text()
# 'Sint velit eveniet. Rerum atque repellat voluptatem quia rerum. Numquam excepturi
# beatae sint laudantium consequatur. Magni occaecati itaque sint et sit tempore. Nesciunt
# amet quidem. Iusto deleniti cum autem ad quia aperiam.
# A consectetur quos aliquam. In iste aliquid et aut similique suscipit. Consequatur qui
# quaerat iste minus hic expedita. Consequuntur error magni et laboriosam. Aut aspernatur
# voluptatem sit aliquam. Dolores voluptatum est.
# Aut molestias et maxime. Fugit autem facilis quos vero. Eius quibusdam possimus est.
# Ea quaerat et quisquam. Deleniti sunt quam. Adipisci consequatur id in occaecati.
# Et sint et. Ut ducimus quod nemo ab voluptatum.'
- 참고 2
from faker import Faker
fake = Faker()
my_word_list = [
'danish','cheesecake','sugar',
'Lollipop','wafer','Gummies',
'sesame','Jelly','beans',
'pie','bar','Ice','oat' ]
fake.sentence()
# 'Expedita at beatae voluptatibus nulla omnis.'
fake.sentence(ext_word_list=my_word_list)
# 'Oat beans oat Lollipop bar cheesecake.'
- 아래와 같이 작성 후 실행하면 파일을 매번 실행할 때 마다 가상 이름, 단어, 문장, 텍스트를 생성할 수 있다.
from faker import Faker
faker = Faker()
# 한국어로 사용할 경우
# faker = Faker("ko_KR")
# 랜덤한 이름 생성
print(faker.name())
print(faker.first_name())
print(faker.last_name())
# 랜덤한 단어 -> 비밀번호에 활용
print(faker.word())
# 랜덤한 한 문장
print(faker.sentence())
# 랜덤한 문장모음
print(faker.text())
12. Faker 사용하여 새로운 유저 만들기
# articles/test.py
class ArticleReadTest(APITestCase):
@classmethod
def setUpTestData(cls):
cls.faker = Faker()
cls.articles = [] # 비어있는 리스트를 만들고 article을 append해 줄 예정
for i in range(10):
cls.user = User.objects.create_user(cls.faker.name(), cls.faker.word()) # 새로운 유저 생성
cls.articles.append(Article.objects.create(title=cls.faker.sentence(), content=cls.faker.text(), user=cls.user))
13. url 연결하기
- 먼저 ArticleDetailView는 아래와 같이 정의해준다.
# articles/views.py
...
class ArticleDetailView(APIView):
def get(self, request, pk):
article = ArticleModel.objects.get(pk=pk)
serializer = ArticleSerializer(article)
return Response(serializer.data)
- article.get_absolute_url을 사용하여 url을 가져오고 있다. get_absolute_url은 models.py에서 지정해준 메서드이다.
# articles/test.py
class ArticleReadTest(APITestCase):
...
def test_get_article(self):
for article in self.articles:
url = article.get_absolute_url()
response = self.client.get(url)
serializer = ArticleSerializer(article).data
for key, value in serializer.items():
self.assertEqual(response.data[key]. value)
# print(key, value)
- article.get_absolute_url은 본인의 pk를 가진 값을 kwargs에 넣어서 article_detail_view에 보낸 후 reverse를 통해 url을 출력할 수 있게 된다.
# articles/models.py
from django.db import models
class Article(models.Model):
...
def get_absolute_url(self):
return reverse('article_detail_view', kwargs={"pk":self.pk})
- urls.py에 path는 아래와 같이 적혀있어야 한다.
# articles/urls.py
urlpatterns = [
# user/
path('', views.ArticleView.as_view(), name="article_view"),
path('<int:pk>/', views.ArticleDetailView.as_view(), name='article_detail_view'),
]
14. serializer method field로 아티클에서 유저네임 받아오기
- article에서 유저id가 아닌 username으로 나타나게 하려 한다. user는 SerializerMethodField를 사용할 때 get_user라는 메서드를 지정하면 get_user를 통해 SerializerMethodField에 값을 전달하게 된다.
# articles/serializers.py
class ArticleSerializer(serializers.ModelSerializer):
user = serializers.SerializerMethodField()
def get_user(self, obj):
return obj.user.username
class Meta:
model = ArticleModel
fields = "__all__"
'DEV > Web 개발' 카테고리의 다른 글
Web 개발 :: DRF 리뷰 _TIL#49 (0) | 2022.11.14 |
---|---|
Web 개발 :: AWS 인스턴스에 도커(Docker) 셋팅 _TIL#48 (1) | 2022.11.11 |
Web 개발 :: 프로젝트 완료 및 KPT 회고록 (0) | 2022.11.08 |
Web 개발 :: Django rest framework, 추천 시스템 _TIL#46 (0) | 2022.11.08 |
파이썬/머신러닝 웹 프로그래밍 :: 11월 첫째주 WIL #10 (0) | 2022.11.07 |
댓글