1부의 긴 이야기의 마지막에서
우리는 requests.get 메서드를 패치하여'blah blah'
라는 스트링을 반환하게 하도록 했다.
그러나 우리는,
1.
Response
객체를 반환 받고 싶다!
2.get
과post
의url
이나header
에 따라 다양한Response
를 반환하게 하고 싶다!
1번은 그리 어렵지 않다.
Response
객체를 만들어주면 된다! 어떻게?
class Response:
def __init__(self, **kwargs):
self.status_code = kwargs['status']
self.data = kwargs['data']
def json(self):
return self.data
res_data = {
'message': 'hello, response',
}
res = Response(status=200, data=res_data)
res.status_code
> 200
res_json = res.json()
res_json['message']
> 'hello, response'
필자는 모든 Response
에서 status_code
, json()
어트리뷰트만 사용한다.
그래서 이 두 기능만 있는 최소한의 Custom Response
객체를 생성했다.
이 Custom Response
를 직접 적용해보자!
# test.py
from unittest import mock
from rest_framework.test import APITestCase
class Response:
def __init__(self, **kwargs):
self.status_code = kwargs['status']
self.data = kwargs['data']
def json(self):
return self.data
@mock.patch("hello_world.requests.get")
class TestAPIHello(APITestCase):
def setUp(self):
self.user = get_user()
do_stuff()
def testcase_00_hello_world(self, mock_get):
mock_get.return_value = Response(
status=200,
data={'user': 'Lee'}
)
response = self.client.get(url="/api/hello-world/")
self.assertEqual(response.status_code, 200)
짜잔!
만약 requests
모듈의 Response
를 반환해주고 싶다면 아래처럼 하면 된다.
# test.py
import requests
from unittest import mock
from rest_framework.test import APITestCase
@mock.patch("hello_world.requests.get")
class TestAPIHello(APITestCase):
def setUp(self):
self.user = get_user()
do_stuff()
def testcase_00_hello_world(self, mock_get):
my_response = requests.models.Response()
response.status_code = 200
response_content = json.dumps({'user': 'Lee'})
response._content = str.encode(response_content)
mock_get.return_value = my_response
response = self.client.get(url="/api/hello-world/")
self.assertEqual(response.status_code, 200)
testcase_00_hello_world
메서드 안에서 실제 Response
모델을 사용했다.
이 방식이 아쉬운 점은, _content라는 protected attribute를 직접 접근해야한다는 것이다.
근데 우리는 모든 requests.get()
메서드가 똑같은 Response
를 보내줄 거라고 기대하지 않는다.
당연히 다양한 분기도 있고 예외처리도 많이 있을 터인데,
우리 테스트 코드에서 이 분기들과 예외처리 부분들을 모두 커버하려면?
상황에 따라 다양한 Response
가 발생해야 한다!
이럴 때 쓰라고 있는 기능이 또 있지요.
Mock의 side_effect
는 예외를 mock 인스턴스에서 예외
를 발생시키게 하거나
mock 인스턴스에 우리가 만든 함수
를 적용시킬 수 있다.
공식문서는 side_effect
를 사용하는 다양한 방법을 설명한다.
아래 간단한 예시 코드가 있다.
from unittest import mock
def hello_world(name):
return f'Hello, world and {name}'
m = mock.Mock()
m.side_effect = hello_world
m('Lee !!')
> Hello, world and Lee !!
가즈아!!
테스트 되는 코드와 테스트 코드를 잘 보자.
# hello_world.py
import requests
from rest_framework.views import APIView
from rest_framework.response import Response
class HelloView(APIView):
def get(self, request):
res1 = requests.get(url='https://some.com/api/user-info/')
# Send one more request !!
res2 = requests.get(url='https://some.com/api/org-info/')
if res1.status_code == res2.status_code == 200:
res1_json = res1.json()
res2_json = res2.json()
data = {
"message": "Hello, World!",
"user": res1_json["user"],
"org": rest2_json["org"],
}
return Response(data, status=200)
data = {
"message": "Error!",
"user": "No user",
}
return Response(data, status=503)
두 url에 request를 보내게 된다.
# test.py
import requests
from unittest import mock
from rest_framework.test import APITestCase
def mock_request_get(**kwargs):
response = requests.models.Response()
response.status_code = 200
url = kwargs.get('url')
if url == 'https://some.com/api/user-info/':
response_content = json.dumps({'user': 'Lee'})
response._content = str.encode(response_content)
elif url == 'https://some.com/api/org-info/':
response_content = json.dumps({'org': 'Avengers'})
response._content = str.encode(response_content)
else:
response.status_code = 400
return response
@mock.patch("hello_world.requests.get")
class TestAPIHello(APITestCase):
def setUp(self):
self.user = get_user()
do_stuff()
def testcase_00_hello_world(self, mock_get):
mock_get.side_effect = mock_request_get
response = self.client.get(url="/api/hello-world/")
self.assertEqual(response.status_code, 200)
외부 api 서버에 request를 보냈을 때 에러가 발생하는 경우를 상정하면 아래와 같은 코드도 가능하다.
# hello_world.py
import requests
from rest_framework.views import APIView
from rest_framework.response import Response
class HelloView(APIView):
def get(self, request):
try:
res1 = requests.get(url='https://some.com/api/user-info/')
res2 = requests.get(url='https://some.com/api/org-info/')
# Return response with status code 500
# if OSError raises.
except OSError:
return Response(status=500)
if res1.status_code == res2.status_code == 200:
res1_json = res1.json()
res2_json = res2.json()
data = {
"message": "Hello, World!",
"user": res1_json["user"],
"org": rest2_json["org"],
}
return Response(data, status=200)
data = {
"message": "Error!",
"user": "No user",
}
return Response(data, status=503)
hello_world.py
에서 requests
를 쓰는 부분에 에러 핸들링하는 코드가 추가되었다.
# test.py
import requests
from unittest import mock
from rest_framework.test import APITestCase
def mock_request_get(**kwargs):
response = requests.models.Response()
response.status_code = 200
url = kwargs.get('url')
if url == 'https://some.com/api/user-info/':
response_content = json.dumps({'user': 'Lee'})
response._content = str.encode(response_content)
elif url == 'https://some.com/api/org-info/':
response_content = json.dumps({'org': 'Avengers'})
response._content = str.encode(response_content)
else:
response.status_code = 400
return response
@mock.patch("hello_world.requests.get")
class TestAPIHello(APITestCase):
def setUp(self):
self.user = get_user()
do_stuff()
def testcase_00_hello_world(self, mock_get):
mock_get.side_effect = mock_request_get
response = self.client.get(url="/api/hello-world/")
self.assertEqual(response.status_code, 200)
def testcase_01_external_api_error(self, mock_get):
mock_get.side_effect = OSError()
response = self.client.get(url="/api/hello-world/")
self.assertEqual(response.status_code, 500)
testcase_01_external_api_error
테스트 메서드가 추가되었다.
여기서는 side_effect
에 함수를 할당하지 않고 예외를 직접 할당했다.
처음으로 길게 써본 글이다. 진이 빠진다.. 원래 이렇게 길게 쓰려고 하진 않았는데, 어쩌다보니 주절주절 길게 쓰게 되었다.
원래는 내가 겪은 과정을 쓰려던 글이었는데 거진 코드 위주의 글이 되어버렸다.. 주륵
사실 mock을 적용하면서 고민이 많았다.
모든 test method에서 각자 side_effect에 할당할 함수를 만들어야 하나?
하나의 API 모듈 안에서
requests
를 쓰는 모든 부분은 파일을 나눠버려서side_effect
에 할당할 함수를 간단하게 만들어줘야 할까?
이런 고민을 한 이유는, 내가 만든 API 모듈이 요청 하나를 처리하는 동안 적어도 두세번의 요청을 외부 API 서버에 보내기 때문이다...
그래서 side_effect
함수를 보면 url에 따른 다양한 처리를 위한 분기문 지옥이 형성되어있다 ㅠㅠ
side effect에 직접 예외 발생 코드를 넣을까 아니면 내가 만든 함수 안에서 분기에 따라 예외를 발생케 할까?
나름대로 최선의 방법으로 테스트 코드에 mock을 적용했는데, 아직 만족스럽지 못하다. 테스트 코드가 깔끔하지 않아서일까?
아니면 내가 만든 API가 애초에 별로라서 일까?
여러 고민을 뒤로하고 일단 글로 남겨보자, 해서 이렇게 쓰게 되었다.
이 글을 보는 다른 이들에게 조금이라도 도움이 되었다면 만족스러워지리라..
이만!