[0712] DRF test.py 활용하기

nikevapormax·2022년 7월 12일
0

TIL

목록 보기
71/116
post-custom-banner

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.status_code, 200)
        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
        
    # a class method to create a Person object by birth year
    @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() # James's age is 29

person1 = Person.fromBirthYear('Micheal', 1995)
person1.display() # Micheal's age is 27
  • 위의 코드를 보면 @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
    
    # a class method to create a Person object by birth year
    # factory method 또는 생성자라고도 부름
    @classmethod
    def fromBirthYear(cls, name, birthYear):
        return cls(name, date.today().year - birthYear)
    
    # a static method to check if a Person is adult or not
    @staticmethod
    def isAdult(age):
        return age > 18
       
person1 = Person('James', 29)
person2 = Person.fromBirthYear('Micheal', 1995)

print(person1.age) # 29
print(person2.age) # 26

print(Person.isAdult(22)) # True
  • 위의 코드를 통해 알 수 있듯이 static methodself와 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']
        
    # 게시글 작성(이미지 o)
    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"], '글 작성 완료')
  • 이미지가 없는 게시글 작성과 똑같은 방식이며, 이미지를 넣었기 때문에 이에 대한 처리를 해준 것이 다른 점이다.
  • 이미지를 처리하는 다른 방식도 있을 수 있다.
profile
https://github.com/nikevapormax
post-custom-banner

0개의 댓글