1. drf에서 테스트코드 작성하기

- 테스트코드란?

작성한 코드들이 원하는 값을 내놓는지 확인하는 코드!
예) unittest(파이썬)

import unittest

class TestStringMethods(unittest.TestCase):
	def test_upper(self):
    	self.assertEqual('foo'.upper(), 'FOO')

결과)

test_isupper (\__main__.TestStringMethods) ... ok

- 테스트 코드를 사용하는 이유!

  1. 버그를 빠르고 쉽게 찾아낼 수 있다.
  2. 시간과 돈을 아낄 수 있다.
  3. 익스트림 프로그래밍에서 중요한 부분을 맡고 있다.
  4. 문서화하여 정보를 제공해준다.
  5. 신뢰도를 높여준다.
  6. 퍼포먼스를 체크할 수 있다.
  7. 코드 커버리지를 보장해준다.(안전한가)
  8. 코드의 복잡도를 낮출 수 있다.

- TDD

2. 프로젝트 설정과 첫 테스트코드

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)

장고의 테스트툴이다!
유닛 테스트 관련 자료 : https://velog.io/@fcfargo/django-Unit-Test

3. 장고에서 쓸 수 있는 테스트툴

4. 회원가입 테스트

from django.urls import reverse
from rest_framework.test import APITestCase
from rest_framework import status


class UserResgistrationAPIViewTestCase(APITestCase):
    def test_registration(self):
        url = reverse("user_view")
        user_data = {
            "email":"test@naver.com",
            "password":"test",
        }
        response = self.client.post(url, user_data)
        self.assertEqual(response.status_code, 201)

reverse로 user_view라는 이름을 가지고 있는 url을 불러온다!
client를 사용하는 이유는 내가 클라이언트가 되어 서버에 요청을 보내는 식으로 테스트 하기 위함!!

5. 로그인 테스트

class UserResgistrationAPIViewTestCase(APITestCase):
	def test_login(self):
        url = reverse("token_obtain_pair")
        user_data = {
            "email":"test@naver.com",
            "password":"test",
        }
        response = self.client.post(url, user_data)
        self.assertEqual(response.status_code, 200)

하지만 결과로 401 에러가 나온다.
그 이유는 test메소드를 실행할 때마다 test메소드 용 db를 생성하고 지우기 때문이다!

6. setUp 메소드

위처럼 오류가 나는 이유는 unit-testing 때문이라고 한다.
모든 테스트가 모두 독립적이어야 하기 때문에!!
연달아서 test를 진행하고 싶으면 따로 함수를 적을것이 아니라 같은 함수내에 바로 작성해야 한다!
하지만 매번 모두 작성하기는 힘드니!! 이 때 사용하는것이 setup이다.

7. setUp을 이용한 로그인 테스트

class LoginUserTest(APITestCase):
    def setUp(self):
        self.data = {"email":"test@naver.com", "password":"test"}
        self.user = User.objects.create_user("test@naver.com", "test")
        
    def test_login(self):
        response = self.client.post(reverse("token_obtain_pair"), self.data)
        self.assertEqual(response.status_code, 200)

setup 관련 자료 : https://docs.djangoproject.com/en/4.0/topics/testing/overview/
테스트코드를 작성할 때 중요한 원칙이 있는데 테스트 대상이 파이썬의 메소드나 장고의 라이브러리가 되어서는 안된다!!

8. 사용자 정보 가져오기 테스트

class LoginUserTest(APITestCase):
    def setUp(self):
        self.data = {'email':'test@naver.com', 'password':'test'}
        self.user = User.objects.create_user('test@naver.com', 'test')
        
    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(
            path=reverse("user_view"),
            HTTP_AUTHORIZATION=f"Bearer {access_token}"
        )
        print(response.data)
        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.data["email"], self.data["email"])


이렇게 사용자의 정보를 터미널에 가져올 수 있다!!

9. setUpTestData

테스트를 하다 보면 user데이터같은 것은 모든 데이터마다 동일하게 유지하고 싶을 가 있다.
setup을 매번 하지 않고 한번 한 후 모든 테스트에 대해서 사용할 수 있는 방법인 setuptestdata에 대해서 알아보자!!

10. class method

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) # 클래스에 이름과 태어난 해를 입력하여 return
    
    def display(self):
        print(self.name + "'s age is:" + str(self.age))
        
        
    person1 = Person.fromBirthYear('John', 1985)
    person1.display()

위처럼 클래스 메소드를 사용하면 인스턴스 없이 바로 클래스이름.함수이름 형식으로 바로 실행할 수 있다.
클래스 메소드에 들어가는 첫 번째 인자는 self가 아닌 본인클래스 이름(cls)이다. (본인의 클래스를 다시 실행 시키는 것이 가능)

11. static method

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.('John', 21)
person2.Person.fromBirthYear('John', 1996)
    
print(Person.isAdult(22))

스테틱 메소드는 self, 본인클래스 둘다 들어가지 않는다.
스테틱 메소드를 사용하지 않고는 class바깥에 함수를 작성해야 하기 때문에 코드를 깔끔하게 작성하기 위해 많이 사용한다.

12. 다시 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']  

여기세어 setUp에 access_token에 대한 코드를 따로 작성해주는 이유는 .client가 클래스메소드가 아니기 때문에 cls.client를 하면 오류가 나기 때문에 이 부분만 setup으로 작성해야 한다!!

13. 로그인 안했을때 게시글 작성 테스트

class ArticleCreateTest(APITestCase):
    @classmethod
    def setUpTestData(cls):
        cls.user_data = {'email':'john@naver.com', 'password':'johnpassword'}
        cls.article_data = {'title':'some title', 'content':'some content'}
        cls.user = User.objects.create_user('john@naver.com', 'johnpassword')
        
    def setUp(self):
        print()
        self.access_token = self.client.post(reverse('token_obtain_pair'), self.user_data).data["access"]    

    # def setUp(self):
    #     self.user_data = {'email':'test@naver.com', 'password':'test'}
    #     self.article_data = {'title':'title', 'content':'hi'}
    #     self.user = User.objects.create_user('test@naver.com', 'test')
    #     self.access_token = self.client.post(reverse('token_obtain_pair'), self.user_data).data['access'] # .client가 클래스메소드가 아니기 때문에 cls.client를 하면 오류가 나기 때문에 이 부분만 setup으로 
        

    def test_fail_if_not_logged_in(self): #테스트 함수에는 무조건 앞에 test를 붙이기! 
        url = reverse('article_view')
        response = self.client.post(url, self.article_data)
        self.assertEqual(response.status_code, 401)

14. 이미지 없는 게시글 작성 테스트

from django.urls import reverse
from rest_framework.test import APITestCase
from rest_framework import status
from user.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)

15. 이미지 포함한 게시글 작성 테스트

이미지를 포함한 게시글을 작성하는 방법은 조금 더 어렵다.
이는 아래와 같이 2단계로 나눠서 진행할 수 있다.
1. 임시 이미지 파일 생성
2. 이미지 파일 전송

from django.urls import reverse
from rest_framework.test import APITestCase
from rest_framework import status
from user.models import  User
# 이미지 업로드
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') # tempt_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_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.status_code, 200)

16. 더미데이터를 위한 Faker 사용법

pip install Faker

from faker import Faker를 한 후 인스턴스를 생성하는 방식으로 사용

from faker import Faker


faker = Faker() # 영어
faker = Faker("ko_KR") # 한국어(name만 된다.)

print(faker.name()) # 랜덤한 이름
print(faker.first_name()) 
print(faker.last_name())
print(faker.word()) # 랜덤한 단어
print(faker.sentence()) # 랜덤한 한 문장
print(faker.text()) # 랜덤한 긴 문장

17. articles 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.articles.append(Article.objects.create(title=cls.faker.sentence(), content=cls.faker.text(), user=cls.user))

18. get absolute url

- models.py에 get absolute url 코드 추가

class Article(models.Model):
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    title = models.CharField(max_length = 50)
    content = models.TextField()
    image = models.ImageField(blank=True, upload_to='%Y/%m/')
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now = True)
    likes = models.ManyToManyField(User, related_name="like_articles") #manytomany 필드는 related_name을 설정안하면 이름이 중복되기 때문에 꼭 설정을 해주어야 한다!!
    
    def __str__(self):
        return str(self.title)
    
    def get_absolute_url(self):
        return reverse('article_detail_view', kwargs={"article_id":self.id})

19. dictionary의 items 메소드

딕셔너리는 키 벨류 형식으로 이루어져 있는데 이 형태는 for문을 돌리기가 어려운 형태이다.
이 때 for문을 돌리면서 키값과 벨류값을 뽑아내고 싶을 때 items를 사용한다.

my_dict = {"뭔가 키값": "뭔가 벨류값", "some key":"some value", "name":"young"}

for key, value in my_dict.items():
    print(key)
    print(value)

키값은 key로 벨류값은 value로 들어가 각자 for문에 돌려진다.
print(key)를 했을 때는 뭔가 키값, some key, name이 나오고
print(value)를 했을 때는 뭔가 벨류값, some value, young이 나온다.

20. 테스트 완성

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.articles.append(Article.objects.create(title=cls.faker.sentence(), content=cls.faker.text(), user=cls.user))

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) # response.data에서 돌아온 데이터에 대해 가지고 있는 key값을 넣으면 그에 대한 value값이 나오게

21. serializermethodfield로 아티클에서 유저네임 받아오기

결과가 email의 pk값이 아닌 email이 나오게 해보자!!

class ArticleSerializer(serializers.ModelSerializer):
    user = serializers.SerializerMethodField()
    comment_set = CommentSerializer(many=True)
    likes = serializers.StringRelatedField(many=True)
    
    def get_user(self, obj):
        return obj.user.email
    
    class Meta:
        model = Article
        fields = "__all__"
profile
개발과 지식의 성장을 즐기는 개발자

0개의 댓글