유닛 테스트는 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()
으로 삭제해야 한다.
테스트 함수는 길고 서술적인 이름을 사용해야 한다.
test_email_error
기능 단위 별로 테스트 Class를 생성해야 되고, 각각의 테스트 Class는 독립적이므로, 매번 초기화 설정을 해줘야 한다.(setUp
함수)
django Unit Test가 이뤄지는 과정(process)을 요약해보면... 다음과 같다.
setUp()
메서드를 통한 테스트 db 생성 및 데이터 추가
my_settings.py
에서 지정된 프로젝트의 데이터베이스에 추가된다.테스트 함수의 self.assertEqual()
메서드를 통해 테스트하려는 코드(로직)의 실제 결괏값과 기댓값을 비교한다. 두 값이 다를 경우 테스트는 실패한다.
tearDown()
메서드를 통한 테스트 db 삭제
Unit Test 실행 코드
Client()
: django에서 제공하는 Client 객체를 활용하면, 서버를 실제로 실행하지 않고도 클라이언트의 입장에서 GET, POST 등의 request를 보냄으로써, views.py에 대한 테스트를 진행할 수 있다.
self.assertEqual()
: views.py의 로직에 의해 도출된 결괏값과 비교할 기댓값을 설정할 수 있다. 주로 응답(response)의 status_code와 JSON 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('users.views.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,
}
)
django Unit Test는 일반적으로 테스트 DB를 생성 및 삭제하며 진행된다. 하지만 이러한 방식으로 Unit Test를 실행이 불가능한 경우가 있는데, 바로 django 프로젝트의 DB로 AWS의 RDS를 사용하는 경우다. 실제로 유닛 테스트를 진행하면 다음과 같은 문구를 띄우면서 에러가 발생한다.
Creating test database for alias 'default'...
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'
로 설정해야 한다.'r'
로 설정해야 한다. 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)