오늘도 개발

Django로 Unit Test해보기 본문

웹 프로그래밍/Django

Django로 Unit Test해보기

Sueeeeeee 2022. 8. 5. 00:54

위코드 Software Testing 세션을 정리한 내용입니다. 

1. Unit Test란?

프로그램을 이루는 가장 작은 유닛을 테스트하는 것.

즉 함수 또는 클래스를 테스트하는 것.

(참고 - Python unittest 모듈로 Unit Test 해보기)

 

2. Django에서 Unit Test하기

Django에서는 Python에서 기본적으로 제공하는 unittest를 사용할 수 있다.

unittest를 사용하면 TestCase 클래스와 Client() 객체를 사용할 수 있다.

 

테스트 코드는 앱 생성 시 자동으로 생성되는 test.py에 작성하고,

실행은 manage.py가 있는 위치에서 다음과 같이 실행한다.

python manage.py test <앱 이름>

 

3. Django에서 Unit Test할 때 기억할 것

1. 테스트 케이스를 위한 클래스를 만들 때는 항상 TestCase 클래스를 상속받아 만들기

2. 테스트 클래스 내부에 작성하는 테스트 메서드는 항상 test_로 시작하기(아니면 파이썬이 인식 못함)

3. Client() 객체를 이용하여 함수 호출하기

 

예 1) 엔드포인트 테스트하기

<views.py>

import json

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

class JustView(View):
    def get(self, request):
        return JsonResponse({'message':'This is for unit test'}, status = 200)

<test.py>

from django.test import TestCase, Client

from units.models import * 

class JustTest(TestCase):
    def setUp(self):
    	# 테스트 코드에서 만드는 오브젝트는 DB에 들어가지 않는다.
        Product.objects.create()
        Product.objects.create()
        Product.objects.create()

    def test_success_just_view_get_method(self):
        # Client() 객체를 사용하면 클라이언트인 척 하고 요청을 보낼 수 있다.
        client = Client()
        
        # 클라이언트가 /just 엔드포인트로 get 요청을 보낸 경우를 시뮬레이션 한다.
        # 호스트는 생략하고 작성한다.
        response = client.get('/just')
		
        # 성공하는 경우를 테스트하는 메서드이기 때문에 200 코드와 성공 메시지를 넣어 확인해본다.
        # 성공 시 응답 코드가 맞게 뜨는지 확인
        self.assertEqual(response.status_code, 200)
        # 성공 시 json 메시지가 맞게 뜨는지 확인
        self.assertEqual(response.json(), {
            'message' : 'This is for unit test'
        })

<터미널> : 테스트 실행

python manage.py test units

# 결과
Found 2 test(s).
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
..
----------------------------------------------------------------------
Ran 2 tests in 0.033s

OK
Destroying test database for alias 'default'...

 

예 2) 모델 테스트하기

<models.py>

class Book(models.Model):
    title = models.CharField(max_length=50)
    authors = models.ManyToManyField('Author', through = 'AuthorBook')

    class Meta:
        db_table = 'books'

class Author(models.Model):
    name = models.CharField(max_length=50)
    email = models.EmailField()

    class Meta:
        db_table = 'authors'

class AuthorBook(models.Model):
    book = models.ForeignKey('Book', on_delete= models.SET_NULL, null=True)
    author = models.ForeignKey('Author', on_delete= models.SET_NULL, null=True)

    class Meta:
        db_table = 'author_book'

<views.py>

import json
import jwt
import requests

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

from .models import Book, Author, AuthorBook

class AuthorView(View):
    # 클라이언트가 작가 정보를 보내면 작가를 DB에 저장하는 api
    def post(self, request):
        data = json.loads(request.body)

        try:
            if Author.objects.filter(name=data['name']).exists():
                return JsonResponse({'message':'DUPLICATED_NAME'}, status = 400)

            Author(
                name = data['name'],
                email = data['email']
            ).save()
            return JsonResponse({'message':'SUCCESS'}, status = 200)

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

class AuthorBookView(View):
    # 패스 파라미터에 책 아이디를 넣어 보내면 책의 저자 리스트를 응답하는 api
    def get(self, request, book_id):
        try:
            if Book.objects.filter(id = book_id).exists():
                book = Book.objects.get(id = book_id)
                authors = list(AuthorBook.objects.filter(book = book).values('author'))
                
                return JsonResponse({'authors':authors}, status = 200)
            
            return JsonResponse({'message':'NO_AUTHOR'}, status = 400)
        
        except KeyError:
            return JsonResponse({'message':'INVALID_KEYS'}, status = 400)

<test.py>

import json
import bcrypt

from django.test import TestCase, Client

from .models import Book, Author, AuthorBook

# Author 뷰의 테스트케이스
class AuthorTest(TestCase):
    def setUp(self):
        # db에 다음과 같은 오브젝트가 이미 존재한다고 설정함
        Author.objects.create(
            id = 1,
            name = 'Snoopy',
            email = 'snoopy@snoopy.com'
        )

        Author.objects.create(
            id = 2,
            name = 'Garfield',
            email = 'garfield@cat.com'
        )

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

    # db에 없는 작가 정보를 post 요청했을 때의 상황
    def test_fail_authorview_post_duplicate_name(self):
        client = Client()
        author = {
            'name' : 'cheddar',
            'email' : 'cheddar@cheddar.com'
        }
        # 클라이언트 인 척 하고 /authors에 포스트 메서드로 요청을 보내봄
        response = client.post('/authors', json.dumps(author), content_type='application/json')

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

    # db에 이미 존재하는 작가 정보를 post 요청했을 때의 상황
    def test_fail_authorview_post(self):
        client = Client()
        author = {
            'name' : 'snoopy',
            'email' : 'snoopy@snoopy.com'
        }
        response = client.post('/authors', json.dumps(author), content_type='application/json')

        self.assertEqual(response.status_code, 400)
        self.assertEqual(response.json(),
            {
                'message' : 'DUPLICATE_NAME'
            }
        )
    
    # 'name' 이라는 키가 아니라 'first_name'이라는 키를 잘못 사용해서 요청한 경우
    def test_fail_authorview_post_invalid_keys(self):
    client = Client()
    author = {
        'first_name'  : 'Snoopy',
        'email'       : 'snoopy@snoopy.com'
    }
    response = client.post('/authors', json.dumps(author), content_type='application/json')

    self.assertEqual(response.status_code, 400)
    self.assertEqual(response.json(),
        {
            'message':'INVALID_KEYS'
        }
    )

# AuthorBook 뷰의 테스트케이스
class AuthorBookTest(TestCase):
    def setUp(self):
        Book.objects.create(
            id    = 1,
            title = 'python'
        )

        Book.objects.create(
            id    = 2,
            title = 'javascript'
        )

        Author.objects.create(
            id    = 1,
            name  = 'Snoopy',
            email = 'snoopy@gmail.com'
        )

        Author.objects.create(
            id    = 2,
            name  = 'Garfield',
            email = 'garfield@gmail.com'
        )

        AuthorBook.objects.create(
            book   = Book.objects.get(id=1),
            author = Author.objects.get(id=1)
        )

        AuthorBook.objects.create(
            book   = Book.objects.get(id=1),
            author = Author.objects.get(id=2)
        )

        AuthorBook.objects.create(
            book   = Book.objects.get(id=2),
            author = Author.objects.get(id=1)
        )

        AuthorBook.objects.create(
            book   = Book.objects.get(id=2),
            author = Author.objects.get(id=2)
        )

    def tearDown(self):
        Book.objects.all().delete()
        Author.objects.all().delete()
        AuthorBook.objects.all().delete()
	
    # /author-books/<int:book_id>에 보낸 요청이 성공적인 경우
    def test_success_authorbook_get(self):
        client   = Client()
        response = client.get('/author-books/1')
        self.assertEqual(response.json(),
            {
                "authors" : [
                    {'author': 1},
                    {'author': 2}
                ],
                         
            }
        )
        self.assertEqual(response.status_code, 200)

 

예 3) Mock(거짓된 값)으로 테스트하기

결제 API, 소셜 로그인 등 실제로 실행할 수 없는 코드를 테스트 할 때 

Mock을 이용하여 테스트한다.

 

<views.py>

import requests

class ...
  def post(self, request):
    ...
    # 카카오 api 엔드포인트 /kakao/user/me에 토큰을 넣어 요청을 보냄 
    response = requests.get('/kakao/user/me', token = {...}, )
    ..
    # 카카오 서버로 온 응답으로부터 id를 가져옴
    kakao_user_id = response.json()["id"].
    ...

 

<test.py>

from django.test   import TestCase, Client
# mock을 사용하기 위해 필요한 모듈
from unittest.mock import patch, MagicMock

class KakaoLoginTest(TestCase):
    # mock을 사용할 때는 모듈에서 제공하는 patch 데코레이터를 붙여야 함
    # users의 views.py에서 requests를 사용한 곳은 실제로 요청을 보내지 않을 것이며
    # 요청하지 않았으므로 받지 못하는 응답은 mock으로 대체한다는 뜻
    @patch("users.views.requests")
    def test_success_kakao_signin_new_user(self, mocked_requests):
        client = Client()

        # 테스트에 사용할 가짜 응답(mock)
        class KaKaoResponse:
            def json(self):
                return {
                  "id":123456789,
                  "kakao_account": { 
                      "profile_needs_agreement": false,
                      "profile": {
                          "nickname": "홍길동",
                  }
                }

        mocked_requests.get = MagicMock(return_value = KaKaoResponse())
        headers             = {"HTTP_Authoriazation": "가짜 access_token"}
        response            = client.get("/users/signin/google", **headers)

        self.assertEqual(response.status_code, 201)