스파르타 코딩클럽 내일배움캠프 AI 웹개발자양성과정 3회차
2022.11.15. 53일차 - TIL
1. Django DRF 강의
1) 목표
Django에서 사용할 수 있는 툴들의 종류에 대해 안다.DRF에서 회원가입 기능을 테스트한다.DRF에서 로그인 기능을 테스트한다.setUp 메소드를 사용할 수 있다.class method를 이해한다.DRF에서 게시글 작성을 테스트한다.Faker를 사용할 수 있다.- get absolute url의 사용 목적과 방법을 이해할 수 있다.
- Dictionary의 items 메소드 사용법을 이해한다.
- serializermethodfield로 연관 테이블의 정보를 가져올 수 있다.
2) DRF에서 테스트코드 작성하기(1)
여태까지 우리의 개발 과정은 구현 -> 웹브라우저로 직접 확인 -> 개선점 찾기 -> 다시 구현 ... 이 순선의 반복이었다. 하지만 이 방법은 다음의 문제점을 수반한다.
- 시간 낭비 : 매번 같은 값을 수동으로 입력해서 테스트해야한다.
- 복잡도 증가 : 새로운 기능이 추가될 때마다 이전 기능을 테스트해야하는데 어디서 문제가 생겼는지 모를 수 있다.
- 허점 : 제대로 돌아가지 않는 기능을 테스트 해보지 못하고 넘어갈 수 있다.
그래서 우리는 테스트 코드를 통해 작성한 코드들이 원하는 값을 내놓는지 확인할 것이다.
3) 프로젝트 설정과 첫 테스트코드
django에서 앱을 생성 시 자동으로 test.py가 생성되는 것을 확인할 수 있다. 간단하게 테스트 코드를 작성해보면 다음과 같다. 테스트 코드는 python manage.py test로 실행시킬 수 있다.
from django.test import TestCase
class TestView(TestCase):
def test_two_is_three(self):
self.assertEqual(2, 3)
def test_two_is_two(self):
self.assertEqual(2, 2)
# 지금 첫번째 메소드가 에러이기 때문에 전체 테스트코드가 에러가 난다.
# test_two_is_three를 삭제하면 코드는 정상 실행된다.
4) 장고에서 쓸 수 있는 테스트툴
각 언어들마다 자신만의 테스트툴이 존재한다.
먼저 Python은 unittest 또는 pytest를 통해 테스트 코드를 작성한다. Django도 결국 python을 기반으로 만들어지기 때문에 이 unittest를 기반으로 만들어진다고 생각해도 무방하다. 기본 Django에서는 TestCase를 이용해 테스트 코드를 작성한다.
하지만 우리는 Django DRF를 이용하고 있기 때문에 이것이 제공하는 툴을 이용해 테스트 코드를 작성할 것이다. Django DRF는 APIRequestFactory를 이용해 테스트 코드를 작성한다.
# Python의 테스크툴 unittest
import unittest
class TestStringMethods(unittest.TestCase):
def test_upper(self):
self.assertEqual('foo'.upper(), 'FOO')
def test_isupper(self):
self.assertTrue('FOO'.isupper())
self.assertFalse('Foo'.isupper())
def test_split(self):
s = 'hello world'
self.assertEqual(s.split(), ['hello', 'world'])
# check that s.split fails when the separator is not a string
with self.assertRaises(TypeError):
s.split(2)
if __name__ == '__main__':
unittest.main()
# 기본 django의 테스트툴 TestCase
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 drf에서 사용하는 APIRequestFactory의 APITestCase
from django.core.urlresolvers import reverse
from rest_framework import status
from rest_framework.test import APITestCase
class AccountTests(APITestCase):
def test_create_account(self):
"""
Ensure we can create a new account object.
"""
url = reverse('account-list')
data = {'name': 'DabApps'}
response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(response.data, data)
5) 회원가입 테스트
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") # 회원가입을 위한 url
user_data = {
# User 모델에 필요한 데이터
"username": "testuser",
"fullname": "테스터",
"email": "test@testuser.com",
"password": "password",
}
response = self.client.post(url, user_data)
self.assertEqual(response.status_code, 200)
URL Reverse : view 함수를 사용해 url을 역으로 계산하는 것 -> urls.py에서 정의한 url pattern의 name만 알고 있다면 view 함수를 통해 매칭되는 url을 찾아 이를 전달하는 방식이다.
Test Client : 더미 웹 브라우저 역할을 하는 python 클래스로, url에서 get 및 post 요청을 시뮬레이션해 응답을 관찰하거나 리디렉션 체인을 확인하거나 특정값을 포함하는 템플릿 컨텍스트로 템플릿이 렌더링되는지 테스트 하는 등의 용도로 사용한다.
assertEqual(a, b) : a와 b가 값이 동일한지 확인
6) 로그인 테스트
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") # 회원가입을 위한 url
user_data = {
# User 모델에 필요한 데이터
"username": "testuser",
"fullname": "테스터",
"email": "test@testuser.com",
"password": "password",
}
response = self.client.post(url, user_data)
self.assertEqual(response.status_code, 200)
def test_login(self):
url = reverse("token_obtain_pair") # 로그인을 위한 url
user_data = {
# User 모델에 필요한 데이터
"username": "testuser",
"fullname": "테스터",
"email": "test@testuser.com",
"password": "password",
}
response = self.client.post(url, user_data)
self.assertEqual(response.status_code, 200)
로그인을 위한 url만 다르게 설정하면 되는데 테스트 결과 존재하지 않는다는 오류 메시지와 함께 에러가 출력된다. 그 이유는 모든 테스트 메소드를 실행할 때마다 장고는 db를 초기화하기 때문이다. 따라서 test_registration 메소드가 끝나자마자 해당 db는 삭제되는 것이다. 이것을 unit-testing이라 한다.
이를 위한 해결 방법이 바로 setUp 메소드이다.
7) setUp 메소드를 이용한 로그인 테스트
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") # 회원가입을 위한 url
user_data = {
# User 모델에 필요한 데이터
"username": "testuser",
"fullname": "테스터",
"email": "test@testuser.com",
"password": "password",
}
response = self.client.post(url, user_data)
self.assertEqual(response.status_code, 200)
class LoginUserTest(APITestCase):
def setUp(self):
self.data = {'username': 'john', 'password': 'johnpassword'} # 로그인을 하기 위한 데이터
self.user = User.objects.create_user('john', 'johnpassword') # UserManager에 맞춰 User정보 생성
def test_login(self):
url = reverse("token_obtain_pair") # 로그인을 위한 url
response = self.client.post(url, self.data)
self.assertEqual(response.status_code, 200)
setUp : test(method마다) 할 때마다 불려서 test 데이터셋을 만들어준다.
tearDown : test가 종료될 때마다 불려서 test 데이터셋을 초기화한다.
8) DRF에서 테스트코드 작성하기(2)
from django.urls import reverse
from rest_framework.test import APITestCase
from rest_framework import status
...
class LoginUserTest(APITestCase):
def setUp(self):
self.data = {'username': 'john', 'password': 'johnpassword'} # 로그인을 하기 위한 데이터
self.user = User.objects.create_user('john', 'johnpassword') # UserManager에 맞춰 User정보 생성
def test_login(self):
url = reverse("token_obtain_pair") # 로그인을 위한 url
response = self.client.post(url, self.data)
self.assertEqual(response.status_code, 200)
def test_get_user_data(self):
url = reverse("token_obtain_pair") # 로그인을 위한 url
access_token = self.client.post(url, self.data).data['access'] # 로그인 후 access token 저장
# 사용자 확인을 위한 url(user_view)에 access token을 header에 포함시켜 사용자 정보를 받아온다.
response = self.client.get(path=reverse("user_view"), HTTP_AUTHORIZATION=f"Bearer {access_token}")
self.asserEqual(response.data['username'], self.data['username']) # db에 있는 사용자와 로그인 사용자가 동일한지 확인
만약 여러개의 앱이 있고, 특정 앱의 test 코드만 실행시키고 싶다면 python manage.py test 앱이름 명령어를 실행한다.
9) setUpTestData
TestCase 한개마다(class마다) 초기 데이터 test 데이터를 만들어준다. 따라서 다양한 메소드 테스트 시 로그인 정보가 필요할 때 사용하면 속도면에서 매우 유용하다.
10) class method
가장 기본이 되는 것은 인스턴스(instance) 메서드로 클래스에 아무 데코레이터(decorator) 없이 메서드를 선언하는 것을 말한다. 이때 첫번째 매개변수로 클래스의 인스턴스가 넘어오는데 이것은 관행적으로 self라고 하며 해당 인스턴스 메서드는 이 self를 통해 인스턴스 속성에 접근하거나 다른 인스턴스 메서드를 호출할 수 있다. 더불어 self를 통해 클래스 속성에 접근하거나 클래스 메서드를 호출하는 것도 가능하다.
...
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
@classmethod
def fromBirthYear(cls, name, birthYear): # cls는 클래스로 Person을 의미한다.
return cls(name, date.today().year-birthYear)
def display(self):
print(self.name + "'s age is: " + str(self.age))
person = Person('Adam', 19)
person.display()
# 정상 출력
person1 = Person.fromBirthYear('John', 1985)
person1.display()
# 정상 출력
보통 클래스는 인스턴스를 생성 후 메서드 이용이 가능하다. 하지만 @classmethod 데코레이터를 이용하면 인스턴스를 생성하지 않고, 메서드 사용이 가능하다. 첫번째 매개 변수로 클래스 인스턴스가 아닌 클래스 자체가 넘어오게 된다. 이 첫번째 매개변수의 이름을 관행적으로 cls라고 하며, 클래스 메서드는 이 cls로 클래스 속성에 접근하거나 클래스 메서드를 호출할 수 있다. 하지만, 인스턴스 속성에 접근하거나 다른 인스턴스 메서드 호출이 불가능하다.
11) static method
...
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
@classmethod
def fromBirthYear(cls, name, birthYear): # cls는 클래스로 Person을 의미한다.
return cls(name, date.today().year-birthYear)
@staticmethod
def isAdult(age):
return age > 18
def display(self):
print(self.name + "'s age is: " + str(self.age))
person = Person('Adam', 19)
person.display()
# 정상 출력
person1 = Person.fromBirthYear('John', 1985)
person1.display()
# 정상 출력
print(Person.isAdult(22))
# 정상 출력
static method는 일반적으로 클래스 밖에 적었을 때의 실행과 다르지 않다. 그냥 코드의 깔끔함을 위해 클래스 내부에 정의해 주는 것 뿐이다. @staticmethod 데코레이터를 사용해 클래스에 메서드를 선언하는 방식으로 정적 메서드는 다른 메서드들과 달리 첫번째 매개변수가 할당되지 않는다. 따라서 정적 메서드 내에서는 인스턴스/클래스 속성에 접근하거나, 인스턴스/클래스 메서드를 호출하는 것이 불가능하다. 명시적으로 넘긴 인자만 접근 가능하다.
12) Article을 위한 setUpTestData
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 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)
client는 classmethod가 아니기 때문에 setUp으로 정의해야한다. 또한 메서드 모두 test로 시작되어 있어야 한다.
13) 이미지 없는 게시글 작성 테스트
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 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)
# 이미지 없는 게시글 작성 테스트
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"], "글 작성 완료!!")
14) 이미지 포함한 게시글 작성 테스트
...
# 임의 이미지 생성
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
...
# 이미지 있는 게시글 생성
def test_create_article_with_image(self):
temp_file = tempfile.NamedTemproryFile() # 임의 파일 생성
temp_file.name = "image.png" # 파일 이름
image_file = get_temporary_image(temp_file) # 임의 이미지 생성
image_file.seek(0) # 파일이기 때문에
self.article_data["image"] = image_file # article_data의 image에 value 등록
response = self.client.post(
path=reverse("article_view"),
data=encoding_multipart(data=self.article_data, boundary=BOUNDARY),
content_type=MULTIPART_CONTENT,
HTTP_AUTHORIZATION=f"Bearer {self.access_token}"
)
self.assertEqual(response.data["message"], "글 작성 완료!!")
15) Faker
소프트웨어 개발 시 프로토타입을 개발하거나 단위 테스트를 작성할 때 가짜 데이터가 필요할 때가 있다. 이때 쉽고 빠르게 가짜 데이터를 얻을 수 있는 방법이 있으니 바로, Faker 라이브러리를 사용하는 것이다.
$ pip install Faker
from faker import Faker
# 가짜 영어 이름
fake = Faker()
fake.name()
# 가짜 한국어 이름
fake = Faker('ko_KR')
fake.name()
# 가짜 영어 주소 생성
fake = Faker()
fake.address()
# 가짜 한국어 주소 생성
fake = Faker('ko_KR')
fake.address()
# 가짜 IP 주소 생성
fake.ipv4_private()
# 가짜 유저 생성
fake.profile()
# 가짜 한 단어 생성
fake.word()
# 가짜 여러 단어 생성
fake.words()
# 가짜 한 문장 생성
fake.sentence()
# 가짜 여러 문장 생성
fake.sentences()
# 가짜 단락 생성
fake.paragraph()
# 가짜 줄글 생성
fake.text()
16) article setuptestdata
...
class ArticleReadTest(APITestCase):
@classmethod
def setUpTestData(cls):
cls.faker = Faker()
cls.articles = []
for i in range(10):
cls.user = User.objects.create_user(cls.faker.name(), cls.faker.word())
cls.article.append(Article.objects.create(title=cls.faker.sentence(), content=cls.faker.text()))
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)
# self.assertEqual(article.title, response.data["title"]) 등과 동일
게시글 상세 보기 url은 pk가 필요하다.
...
class Article(models.Model):
...
def get_absolute_url(self):
return reverse('article_detail_view', kwargs={"pk": self.pk})
이때 items는 python에서 딕셔너리 요소 하나하나에 접근할 때 사용한다.
17) get absolute url
18) Dictionary의 items메소드
19) 테스트 완성
20) serializermethodfield로 article에서 username 받아오기
article에서 출력되는 user는 id값으로 출력된다. 보기 편하게 username이 출력되는 것으로 바꾸고자 한다면 article 앱의 serializers.py 내용을 다음과 같이 변경한다.
...
class ArticleSerializer(serializers.ModelSerializer):
user = serializers.SerializerMethodField()
def get_user(self, obj):
return obj.user.username
...
'개발일지 > AI 캠프' 카테고리의 다른 글
내일배움캠프 AI - 55일차 TIL, 2022.11.17 (0) | 2022.11.17 |
---|---|
내일배움캠프 AI - 54일차 TIL, 2022.11.16 (0) | 2022.11.17 |
내일배움캠프 AI - 52일차 TIL, 2022.11.14 (0) | 2022.11.15 |
내일배움캠프 AI - 51일차 TIL, 2022.11.11 (0) | 2022.11.14 |
내일배움캠프 AI - 11주차 WIL (0) | 2022.11.14 |