django - Unit Test

김영훈·2021년 7월 3일
2

Django

목록 보기
8/11

Unit Test란?

  • 유닛 테스트란 내가 작성한 코드의 가장 작은 단위라 할 수 있는 함수를 테스트하는 메서드다. 유닛 테스트를 활용하면 적은 비용으로 코드의 문제점을 빠르게 파악하고 바로잡을 수 있기 때문에, 코드의 추가·변경과, 유지·보수를 효율적으로 할 수 있게 된다.

Testing Pyramid

  • 유닛 테스트는 Google Test Automation Conference에서 제안한 3가지 시스템 테스트 방법 중 하나다. 시스템을 테스트하는 3가지 방법은 테스트 피라미드라고 하는데, 이미지로 표현하면 아래와 같다.

  • 각각의 테스트 방법이 가진 특성에 대해 정리해보면 다음과 같다.

    • End-To-End Testing / UI Testing(10% 비율로 구현하는 것을 권장)

      • 크롬 브라우저를 띄운 다음 본인이 만든 검색 페이지로 들어가서, 직접 검색하고, 내용이 제대로 나오는지 화면상에서 확인하거나, 회원가입이 되는지 직접 브라우저에서 값을 입력하여 확인하는 방법

      • 테스트 중에서 가장 어렵고 자동화하기 까다롭다.

      • 시간이 오래 걸리고, 비용이 많이 든다.

    • Integration Testing(20%)

      • 최소 두 개 이상의 클래스 또는 서브 시스템의 결합을 테스트하는 방법

      • 가령, Postman이나 httpie로 호출해서 JSON response 객체가 제대로 출력되는지 확인하는 것이 이에 해당한다.

      • E2E Testing 다음으로 공수가 많이 든다.

    • Unit Testing(70%)

      • 가장 쉬우며 비용 대비 효과가 좋다.

      • 자동화하기 쉽다.

  • 결과적으로, Unit Testing비용이 적게 들고, 자동화가 쉬워서 테스트 속도가 빠르다 또한 새로운 기능을 구현할 때 유닛 테스트를 잘 작성해 두면, 추후 regression 테스트를 반복적으로 실행함으로써 유지 보수가 쉬워진다. 한마디로 Unit Testing을 하지 않을 이유가 없다.

    • Regression 테스트(회귀 테스트)란?

      • 코드가 변경됐을 경우, 이전에 사용했던 테스트 케이스실행하여, 새로운 기능, 버그 수정, 기존 기능의 변경 사항 등이 제대로 작동하는지 확인하는 테스트 유형이다.

일반 원칙

  • 유닛 테스트를 구현하기 위해선 지켜야 할 원칙이 있다. 여러 원칙 중 몇 가지 만을 추려 소개하면 다음과 같다.

    • 테스트 유닛각 기능의 가장 작은 단위에 집중하여, 해당 기능이 정확히 동작하는지를 증명해야 한다.

    • setUp()을 통해 새로운 데이터를 생성하여 각각의 테스트를 실행해야 하며, 실행 결과는 tearDown()으로 삭제해야 한다.

    • 테스트 함수길고 서술적인 이름을 사용해야 한다.

      • ex) test_email_error
    • 기능 단위 별로 테스트 Class를 생성해야 되고, 각각의 테스트 Class는 독립적이므로, 매번 초기화 설정을 해줘야 한다.(setUp함수)

django로 Unit Test 실행하기

  • django Unit Test가 이뤄지는 과정(process)을 요약해보면... 다음과 같다.

    1. setUp()메서드를 통한 테스트 db 생성데이터 추가

      • 테스트 db는 my_settings.py에서 지정된 프로젝트의 데이터베이스에 추가된다.
    2. 테스트 함수의 self.assertEqual() 메서드를 통해 테스트하려는 코드(로직)의 실제 결괏값기댓값을 비교한다. 두 값이 다를 경우 테스트는 실패한다.

    3. tearDown()메서드를 통한 테스트 db 삭제

  • Unit Test 실행 코드

    • Client(): django에서 제공하는 Client 객체를 활용하면, 서버를 실제로 실행하지 않고도 클라이언트의 입장에서 GET, POST 등의 request를 보냄으로써, views.py에 대한 테스트를 진행할 수 있다.

    • self.assertEqual(): views.py의 로직에 의해 도출된 결괏값과 비교할 기댓값설정할 수 있다. 주로 응답(response)의 status_codeJSON Response 객체를 기댓값으로 설정한다.

    • self.assert.contains(response 객체, text, status_code):views.py의 로직에 의해 결괏값이 제대로 출력되는 지 확인하기 위한 조건을 설정한다. 만약 결괏값이 assert.contains()의 인자로 받은 값을 포함하고 있다면, 테스트가 정상 처리 된다.

      • text: 결괏값이 포함해야 할 문자열을 설정

      • status_code: 결괏값이 반환해야 할 상태코드를 설정

      • 예시: self.assertContains(response=response, text='SUCCESS', status_code=200)

import bcrypt
import jwt
import json
from unittest.mock import patch, MagicMock

from django.test   import Client, TestCase

from users.models  import User
import my_settings

class KakaoSignInTest(TestCase): 
   def setUp(self):
        password          = '1234'
        hashed_password   = bcrypt.hashpw(
                password.encode('UTF-8'), bcrypt.gensalt()
            ).decode('UTF-8')
        User.objects.create(
            name          = '침착맨',       # id 값은 자동 생성되지 않으므로, 필요한 경우 필드와 값을 직접 입력해줘야 한다.
            email         = 'test@gmail.com',
            password      = hashed_password,
            phone_number  = '01012341111',
           )

    def tearDown(self):
        User.objects.filter(name='침착맨').delete()

    def test_key_error(self):
        c = Client()
        header = {}   # POST 요청(request)의 header를 비어있는 객체로 설정했다.
        
        response = c.post('/users/kakao-signin', content_type='application/json', **header)

        self.assertEqual(response.status_code, 400)  # POST 요청에 대한 응답(response)의 예상 상태 코드를 400으로 설정 
        self.assertEqual(response.json(),
                {
                    "message"   : "KEY_ERROR",
                }
            ) # POST 요청에 대한 응답(response)의 예상되는 JSON response 객체를 'KEY_ERROR 메시지'로 설정 
  • patch decorator를 활용해 Kakao API 요청 테스트하기

    • patch deocorator란?

      • patch() decorator를 이용하면 특정 모듈함수클래스가짜 객체(magic mock) 인스턴스대체할 수 있다.
    • 주요 사용 용도

      • 외부 데이터(ex.Kakao API)에 의존하는 코드테스트를 할 때 유용하게 사용된다. 실제로 외부 데이터를 호출request)하여 응답(response)을 받으면 테스트 속도가 느려지므로, mocking을 통해 생성한 가짜 데이터특정 함수결괏값으로 대체하면, 테스트 비용을 줄일 수 있다.
    • @patch('users.views.requests')

      • users app의 모듈 views.py 에서 사용되는 requests 함수(메서드)를 가짜 객체 인스턴스(MagicMock())로 대체하겠다는 의미 —> test 함수의 매개변수(mocked_request)에, views.py에서 사용되는 requests 함수할당되고, 이를 활용하여 가짜 객체를 requests 함수로 대체할 수 있게 된다. —> 함수의 대체는 requests 함수의 결괏값 = 가짜 객체의 결괏값를 의미한다.
    • Unit Test에서는 header에 담을 token의 key를 Authorization으로 적으면 작동하지 않고, HTTP_Authorization으로 적어야 한다.

    • json.dumps(dictionary 객체): python의 requests.post() 요청에서 body에 데이터 객체를 JSON data 형태로 담을 때 사용한다.

    • JSONDecodeError : json.loads() 메서드로 역 직렬화한(읽은) 데이터가 유효한 JSON 문서가 아닌 경우 발생하는 에러

    • python manage.py test <test할 app name>: django Unit Test 실행 명령어

    • 참고 사이트

    @patch('users.views.requests') 
    def test_kakao_signin_success(self, mocked_request): # 매개변수 mocked_request 추가

        class FakeResponse: # MagickModck() 인스턴스의 결괏값으로 사용될 클래스 생성
            def json(self):
                return {
                    "id" : 123456,
                    "kakao_account": {"email":"test@gmail.com"}
                }
        
        mocked_request.get = MagicMock(return_value = FakeResponse()) # requests.get() 메서드의 응답(결괏값)에 MagickModck() 인스턴스의 결괏값을 할당
        token = jwt.encode({
                'user_id':User.objects.get(email="test@gmail.com").id}, 
                my_settings.SECRET_KEY, algorithm="HS256"
            )  # 기댓값 설정에 사용될 데이터는 testㄴ.py에서 직접 생성해야 한다.

        c = Client()
        header = {'HTTP_Authorization':'fake_token.1234'}
        
        response = c.post('/users/kakao-signin', content_type='application/json', **header)
        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.json(),
                {
                    "message" : "SUCCESS",
                    "token"   : token,
                }
            )

Test DB 없이 Unit Test 실행하기

  • django Unit Test는 일반적으로 테스트 DB를 생성 및 삭제하며 진행된다. 하지만 이러한 방식으로 Unit Test를 실행이 불가능한 경우가 있는데, 바로 django 프로젝트의 DB로 AWS의 RDS를 사용하는 경우다. 실제로 유닛 테스트를 진행하면 다음과 같은 문구를 띄우면서 에러가 발생한다.

    Creating test database for alias 'default'...

  • 해결 방법은???
    1. 가장 간단한 해결 방법은 테스트를 할 때 RDS 대신 로컬 DB를 사용하는 것이다.
    2. RDS를 굳이 사용하고 싶다면, 테스트 DB의 생성 없이 Unit Test가 이뤄지도록 설정을 바꿔주는 방법도 있다.
      • 자세한 방법은 아래의 사이트를 참고하면 된다.
      • 참고 사이트

기타 알아두면 좋은 것들

  • json.dumps() vs json.loads()

    • json.dumps(): dictionary type 객체를 string type 으로 변환
    • json.loads(): json 형식의 데이터를 dictionary type 으로 변환
  • json.dumps()로 dictionary type 객체를 형변환하여 POST 요청을 보낼 때, content_type='applications/json을 입력하여 body 내부 데이터 형식을 json으로 설정해야 한다.

  • form-data 로 body에 데이터를 담아 POST 요청을 보낼 때에는, content-type을 따로 설정하지 않아도 된다.

  • 파일을 첨부하여 POST 요청을 보내는 방법

    • with open('파일 경로', '파일 열기 모드') 으로 파일 열기를 실행한 뒤, key:value형태로 body에 데이터를 추가하면 된다. 파일 열기 모드의 기본 값은 'r'이다.
    • 바이너리 파일 : 파일 열기 모드를 'rb' 로 설정해야 한다.
      • exe, dll, zip, rar, mp3, mpg, jpg, png
    • 텍스트 파일 : 파일 열기 모드를 'r' 로 설정해야 한다.
      • txt, c, cpp, bat, java, html, xml, css
        with open('/Users/younghun/Downloads/IMG_1749.JPG', 'rb') as myfile1, open('/Users/younghun/Downloads/얼평.jpeg', 'rb') as myfile2 :
            response = c.post('/boards/board-write', {"json":json.dumps(body), "filename": [myfile1, myfile2]}, **header)   
profile
Difference & Repetition

0개의 댓글