Django ⎮ Unit Test

Chris-Yang·2021년 10월 31일
1

Django

목록 보기
6/7
post-thumbnail

> Unit Test 분류

Unit Test는 일반적으로 나의 API만을 테스트하는 경우가 있고
kakao 소셜 로그인과 같이 외부 API를 이용하는 경우가 있습니다.

Unit Test에서는 외부 API를 이용하므로써 발생하는 제반 비용과 시간을
들일 필요 없이 Mock Data를 이용합니다.

따라서 두가지 경우로 나누어 예시 Test Case를 올려보겠습니다.

System Test 및 Unit Test에 대한 개념은 아래 링크로 첨부하겠습니다.

🥕 System Test
https://velog.io/write?id=fc537ffa-58c5-4b18-bd6e-947ae57d3bed





> 일반 Unit Test

▶︎ code

- models.py

from django.db   import models

from core.models import TimeStampModel

class Brand(TimeStampModel):
    name = models.CharField(max_length = 20)

    class Meta:
        db_table = 'brands'

class Product(TimeStampModel):
    brand         = models.ForeignKey('Brand', on_delete = models.CASCADE)
    name          = models.CharField(max_length = 50)
    model_number  = models.CharField(max_length = 200)
    release_price = models.PositiveIntegerField()

    class Meta:
        db_table = 'products'

class ProductSize(TimeStampModel):
    product = models.ForeignKey('Product', on_delete = models.CASCADE)
    size    = models.ForeignKey('Size', on_delete = models.CASCADE)

    class Meta:
        db_table = 'product_sizes'

class Size(TimeStampModel):
    size = models.PositiveIntegerField()

    class Meta:
        db_table = 'sizes'

class ProductImage(TimeStampModel):
    product   = models.ForeignKey('Product', on_delete = models.CASCADE, null = True)
    image_url = models.CharField(max_length = 1000)

    class Meta :
        db_table = 'product_images'

class Wishlist(TimeStampModel):
    product = models.ForeignKey('Product', on_delete = models.CASCADE)
    user    = models.ForeignKey('users.User', on_delete = models.CASCADE)

    class Meta :
        db_table = 'wishlists'

- views.py

import json
from json.decoder import JSONDecodeError

from django.http import JsonResponse
from django.views import View

from products.models import Product, Wishlist
from users.utils  import login_decorator

class WishList(View):
    @login_decorator
    def post(self, request):
        try:
            user_id      = request.user.id
            wish_product = request.GET.get('product_id')

            if not Product.objects.filter(id = wish_product).exists():
                return JsonResponse({'message:' : 'INVALID_PRODUCT_ID'}, status = 404)

            if not Wishlist.objects.filter(user_id = user_id, product_id = wish_product).exists():
                Wishlist.objects.create(user_id = user_id, product_id = wish_product)

                wish_count = Wishlist.objects.filter(product_id = wish_product).count()

                return JsonResponse({'message' : 'WISH_CREATE_SUCCESS', 'wish_count' : wish_count}, status = 201)

            Wishlist.objects.get(user_id = user_id, product_id = wish_product).delete()

            wish_count = Wishlist.objects.filter(product_id = wish_product).count()

            return JsonResponse({'message' : 'WISH_DELETE_SUCCESS', 'wish_count' : wish_count}, status = 200)
        
        except KeyError:
            return JsonResponse({'message' : 'KEY_ERROR'}, status = 400)

        except JSONDecodeError:
            return JsonResponse({'message' : 'JSON_DECODE_ERROR'}, status = 400)

    @login_decorator
    def get(self, request):
        try:
            user_id   = request.user.id
            wish_list = Wishlist.objects.filter(user_id = user_id)

            results = [{
                'id'    : wish.product.id,
                'brand' : wish.product.brand.name,
                'name'  : wish.product.name,
                'price' : wish.product.release_price,
                'image' : 
                [
                    {
                        'thumbnail' : img.image_url
                    } for img in wish.product.productimage_set.all()[:1]]
                } for wish in wish_list]

            return JsonResponse({'results' : results}, status = 200)

        except JSONDecodeError:
            return JsonResponse({'message' : 'JSON_DECODE_ERROR'}, status = 400)

class WishFlag(View):
    @login_decorator
    def get(self, request):
        try:
            user_id      = request.user.id
            wish_product = request.GET.get('product_id')

            if not Product.objects.filter(id = wish_product):
                return JsonResponse({'message:' : 'INVALID_PRODUCT_ID'}, status = 404)

            wish_count = Wishlist.objects.filter(product_id = wish_product).count()

            check_my_wish = True

            if not Wishlist.objects.filter(product_id = wish_product, user_id = user_id):
                check_my_wish = False

            results = {
                'wish_count'    : wish_count,
                'check_my_wish' : check_my_wish,
            }

            return JsonResponse({'results' : results}, status = 200)
    
        except KeyError:
            return JsonResponse({'message' : 'KEY_ERROR'}, status = 400)

- tests.py

import json

from django.test import TestCase, Client, client

from .models      import Product, Wishlist, Brand, ProductImage
from users.models import User
from my_settings  import SECRET_KEY, ALGORITHMS

client = Client()

class WishListTest(TestCase):
    def setUp(self):
        Brand.objects.create(id=1, name='나이키')
        Product.objects.create(id=1, brand_id=1, name='조단1', model_number=1234, release_price=100000)
        Product.objects.create(id=2, brand_id=1, name='조단2', model_number=4321, release_price=200000)
        ProductImage.objects.create(id=1, image_url='예쁜조단', product_id=1)
        User.objects.create(id=1, email='abcdefg1@google.com')
        Wishlist.objects.create(id=1, user_id=1, product_id=1)

        global wish_count1, wish_count2, headers
        wish_count1 = Wishlist.objects.filter(product_id=1).count()
        wish_count2 = Wishlist.objects.filter(product_id=2).count()
        access_token = jwt.encode({'user_id' : 1}, SECRET_KEY, ALGORITHMS)
        headers      = {'HTTP_AUTHORIZATION': access_token}

    def tearDown(self):
        User.objects.all().delete()
        Brand.objects.all().delete()
        Product.objects.all().delete()
        Wishlist.objects.all().delete()
        ProductImage.objects.all().delete()

    def test_wishlist_delete_success(self):
        response = client.post('/products/wishlist?product_id=1', content_type='application/json', **headers)

        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.json(), {'message': 'WISH_DELETE_SUCCESS', 'wish_count' : wish_count1})

    def test_wishlist_create_success(self):
        response = client.post('/products/wishlist?product_id=2', content_type='application/json', **headers)

        self.assertEqual(response.status_code, 201)
        self.assertEqual(response.json(), {'message': 'WISH_CREATE_SUCCESS', 'wish_count' : wish_count2})

    def test_wishlist_create_fail(self):
        response = client.post('/products/wishlist?product_id=10', content_type='application/json', **headers)
        
        self.assertEqual(response.status_code, 404)
        self.assertEqual(response.json(), {'message:' : 'INVALID_PRODUCT_ID'})

    def test_wishlist_get_success(self):
        response = client.get('/products/wishlist', **headers)

        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.json(),
            {
                'results' : [{
                    'brand' : '나이키', 
                    'name'  : '조단1',
                    'price' : 100000,
                    'image' : [{'thumbnail': '예쁜조단'}],
                }],
            }
        )

    def test_wishlist_get_fail(self):
        response = client.get('/products/wishlist', **headers)

        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.json(),
            {
                'results' : [{
                    'brand' : '나이키', 
                    'name'  : '조단1',
                    'price' : 100000,
                    'image' : [{'thumbnail': '예쁜조단'}],
                }],
            }
        )

class WishFlag(TestCase):
    def setUp(self):
        Brand.objects.create(id=1, name='나이키')
        Product.objects.create(id=1, brand_id=1, name='조단1', model_number=1234, release_price=100000)
        Product.objects.create(id=2, brand_id=1, name='조단2', model_number=4321, release_price=200000)
        ProductImage.objects.create(id=1, image_url='예쁜조단', product_id=1)
        User.objects.create(id=1, email='abcdefg1@google.com')
        User.objects.create(id=2, email='abcdefg2@google.com')
        User.objects.create(id=3, email='abcdefg3@google.com')
        User.objects.create(id=4, email='abcdefg4@google.com')
        User.objects.create(id=5, email='abcdefg5@google.com')
        Wishlist.objects.create(id=1, user_id=1, product_id=1)
        Wishlist.objects.create(id=2, user_id=2, product_id=1)
        Wishlist.objects.create(id=3, user_id=3, product_id=1)
        Wishlist.objects.create(id=4, user_id=4, product_id=1)
        Wishlist.objects.create(id=5, user_id=5, product_id=1)

        global wish_count, headers
        wish_count   = Wishlist.objects.filter(product_id=1).count()
        access_token = jwt.encode({'user_id' : 2}, SECRET_KEY, ALGORITHMS)
        headers      = {'HTTP_AUTHORIZATION': access_token}

    def tearDown(self):
        User.objects.all().delete()
        Brand.objects.all().delete()
        Product.objects.all().delete()
        Wishlist.objects.all().delete()
        ProductImage.objects.all().delete()

    def test_wishflag_get_success(self):
        response = client.get('/products/wishflag?product_id=1', **headers)

        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.json(), 
            {
                'results' : {
                    'wish_count' : wish_count,
                    'check_my_wish' : True,
                }
            }
        )

    def test_wishflag_get_fail(self):
        response = client.get('/products/wishflag?product_id=10', **headers)

        self.assertEqual(response.status_code, 404)
        self.assertEqual(response.json(), {'message:' : 'INVALID_PRODUCT_ID'})


▶︎ Divide & Conquer

- setUp(self)

setUp() 함수는 우리가 설계한 Table들에 임의로 데이터를 넣는 곳입니다.

이것이 의미하는 바는 실재 우리의 DB에 있는 데이터를 사용하는 것이 아니라는 점입니다!

따라서 어떤 경우에서는 매우 귀찮은 작업이기도 하지만
반대로 DB에 데이터가 없어도 되며 BD활성화에 구애받지 않습니다.



- tearDown(self)

setUp() 함수에서 만들어진 데이터를 삭제해줍니다.

다른 테스트 셋에 영향을 주지 않기 위해 반드시 필요한 작업입니다.


- test_wishlist_delete_success(self)

함수명 가장 앞에 test라고 명시해주지 않으면 인식하지 못합니다.

test 이후에는 해당 함수가 하고자 하는 테스트 내용을 명시하면 됩니다.

실재로 우리가 작성한 view(API)의 로직에 따라 테스트됨을 이해해야 합니다.


해당 함수 안의 내용은 클라이언트 입장에서 바라본 내용입니다.

response = client...는 클라이언트가 요청하는 내용이며
Django Unit Test에서 정한 규칙에 따라 작성되는 Script입니다.

client.post(), client.get()등으로 HTTP method를 구분하고
그에 따른 내용을 매개변수로 작성하면 됩니다.

self.assertEqual()에는 우리가 작성한 view(API)에 의해 클라이언트가
리턴받는 내용을 적으면 됩니다.

Postman이나 httpie를 써본 분은 이해가 쉽게 되실것입니다.


상단 client = Client()나 setUp 함수에 따로 빼놓은 headers 등 때문에
처음 접하시는 분들은 다소 헷갈리실 수도 있겠지만 저는 여러번 적기 귀찮아서
글로벌 변수로 적용하였을 뿐입니다.

그냥 아래와 같이 각각의 테스트 함수 안에 넣어두어도 상관 없습니다.

def test_wishlist_delete_success(self):
    client       = Client()
    wish_count   = Wishlist.objects.filter(product_id=1).count()
    access_token = jwt.encode({'user_id' : 1}, SECRET_KEY, ALGORITHMS)
    headers      = {'HTTP_AUTHORIZATION': access_token}
    response     = client.post('/products/wishlist?product_id=1', content_type='application/json', **headers)

    self.assertEqual(response.status_code, 200)
    self.assertEqual(response.json(), {'message': 'WISH_DELETE_SUCCESS', 'wish_count' : wish_count})

그 다음 계속되는 함수들도 같은 방식으로 작성된 것들입니다.





> Mock Data를 이용한 Unit Test

- models.py

from django.db   import models

from core.models import TimeStampModel

class User(TimeStampModel):
    email        = models.CharField(max_length = 100, unique = True)
    password     = models.CharField(max_length = 200, null = True)
    kakao_id     = models.CharField(max_length = 100, null = True)
    name         = models.CharField(max_length = 20, null = True)
    phone_number = models.CharField(max_length = 20, null = True)
    address      = models.CharField(max_length = 200, null = True)
    point        = models.PositiveIntegerField(default = 1000000)
    class Meta:
        db_table = 'users'

- views.py

import jwt
import requests
from json.decoder import JSONDecodeError

from django.http  import JsonResponse
from django.views import View

from users.models import User
from my_settings  import SECRET_KEY, ALGORITHMS

class KakaoLogin(View):
    def get(self, request):
        try: 
            token = request.headers.get('Authorization')

            if token == None:
                return JsonResponse({'messsage': 'INVALID_TOKEN'}, status=401)

            kakao_account = requests.get('https://kapi.kakao.com/v2/user/me', headers = {'Authorization': f'Bearer {token}'}).json()

            if not User.objects.filter(kakao_id=kakao_account['id']).exists():
               user = User.objects.create(
                    kakao_id = kakao_account['id'],
                    email    = kakao_account['kakao_account']['email'],
                    name     = kakao_account['kakao_account']['profile']['nickname']
               )
            user = User.objects.get(kakao_id=kakao_account['id'])

            access_token = jwt.encode({'user_id': user.id}, SECRET_KEY, algorithm=ALGORITHMS)

            return JsonResponse({'access_token': access_token}, status=201)

        except KeyError:
            return JsonResponse({'message': 'KEY_ERROR'}, status=400)
        
        except JSONDecodeError:
            return JsonResponse({'message': 'JSON_DECODE_ERROR'}, status=400)

        except jwt.DecodeError:
            return JsonResponse({'message': 'JWT_DECODE_ERROR'}, status=400)

        except ConnectionError:
            return JsonResponse({'message': 'CONNECTION_ERROR'}, status=400)

- tests.py

import json, jwt

from django.test import TransactionTestCase, Client

from unittest.mock import patch, MagicMock

from users.models import User
from my_settings  import SECRET_KEY, ALGORITHMS

class KakaoLoginTest(TransactionTestCase):
    def setUp(self):
         User.objects.create(
            id       = 1,
            email    = 'maxsummer256@gmail.com',
            kakao_id = 123,
            point    = 100000000
        )

    def tearDown(self):
        User.objects.all().delete()

    @patch('users.views.requests')
    def test_kakao_login_success_account_exist(self, mock_data_request):
        client = Client()

        class MockDataResponse:
            def json(self):
                return {
                    'id': 123,
                    'kakao_account': { 
                        'email' : 'maxsummer256@gmail.com'
                    }
                }

        mock_data_request.get = MagicMock(return_value=MockDataResponse())
        header                = {'HTTP_Authorization' : 'access_token'}
        response              = client.get('/users/login/kakao', content_type='application/json', **header)
        login_token           = jwt.encode({'user_id': 1}, SECRET_KEY, ALGORITHMS)

        self.assertEqual(response.status_code, 201)
        self.assertEqual(response.json(), {'access_token': login_token})

class KakaoSignUpTest(TransactionTestCase):
    @patch('users.views.requests')
    def test_kakao_login_success_account_nonexist(self, mock_data_request):
        client = Client()

        class MockDataResponse:
            def json(self):
                return {
                    'id': 123,
                    'kakao_account': { 
                        'email' : 'maxsummer256@gmail.com'
                    }
                }

        mock_data_request.get = MagicMock(return_value=MockDataResponse())
        header                = {'HTTP_Authorization' : 'access_token'}
        response              = client.get('/users/login/kakao', content_type='application/json', **header)
        login_token           = jwt.encode({'user_id': 6}, SECRET_KEY, ALGORITHMS)

        self.assertEqual(response.status_code, 201)
        self.assertEqual(response.json(), {'access_token': login_token})

    def test_kakao_login_fail(self):
        client   = Client()

        header   = {'No_Authorization' : ''}
        response = client.get('/users/login/kakao', content_type='application/json', **header)
        
        self.assertEqual(response.status_code, 401)


▶︎ Divide & Conquer

from django.unittest import mock, patch을 작성해 모듈을 불러옵니다.

Test 함수 위에 @patch("users.views.requests")라는 데코레이터를 붙여줍니다.
(users 부분은 자신의 app 이름에 따라 바꿔줍니다.)

Test 함수의 매개변수로 self 다음에 mock_data_request를 넣어줍니다.


아래 내용은 kakao로부터 response받는 내용을 Mock Data로 만든 부분입니다.

class MockDataResponse:
    def json(self):
        return {
            'id': 123,
            'kakao_account': { 
                'email' : 'maxsummer256@gmail.com'
            }
        }

아래 내용은 위에서 만든 Mock Data를 받아오는 Django의 규칙이니
이해하려 하지 말고 그렇구나~ 하고 자신의 의도에 맞춰 작성합니다.

mock_data_request.get = MagicMock(return_value=MockDataResponse())

위 두개의 작업이 views.py에 적힌 코드들 중 아래 코드를 대체해줍니다.

kakao_account = requests.get('https://kapi.kakao.com/v2/user/me', 
	headers = {'Authorization': f'Bearer {token}'}).json()

다른 코드들은 일반 Unit Test의 내용들과 동일합니다.

profile
sharing all the world

0개의 댓글