실전에서 더 많이 쓰이는 unittest.mock
모듈의 patch()
데코레이터를 이용한 Unit Test
를 알아보자.
unittest.mock
모듈의 patch()
데코레이터를 이용하면 특정 모듈의 함수나 클래스를 가짜(mock) 객체 MagicMock
인스턴스로 대체할 수 있다.
이 과정을 흔히 mocking 또는 patching 이라고 한다.
Unit Test
를 작성할 때 외부 서비스에 의존하지 않고 독립적으로 실행이 가능한 Unit Test
를 작성하기 위해 사용되는 테스팅 기법이다.
unittest.mock
모듈의 patch()
데코레이터는 특정 범위 내에서만 mocking 이 가능하도록 해준다.
일반적으로 아래 코드와 같이 patching 이 필요한 Unit Test
메서드에 patch()
데코레이터를 선언해줌으로써 해당 매서드 내에서만 patching 이 이뤄지게 한다.
test_us.py
from unittest import TestCase, main
from unittest.mock import patch
def hello():
return "Hello!"
class TestMe(TestCase):
@patch("__main__.hello", return_value="Mock!")
def test_hello(self, mock_hello):
self.assertEqual(hello(), "Mock!")
self.assertIs(hello, mock_hello)
mock_hello.assert_called_once_with()
if __name__ == "__main__":
main()
위 예제를 보면 원래 "Hello!"
을 리턴하는 hello()
함수가 "Mock!"
를 대신 리턴하도록 @patch()
데코레이터로 patching 을 하고 있다.
@patch("__main__.hello", return_value="Mock!")
@patch()
데코레이터는 첫번째 인자로 patching 할 메서드를 package.module.Class.method
형태의 문자열로 받는다.
본 예제에서는 patching 할 메서드가 같은 모듈에 있기 때문에 __main__
모듈명을 사용하고 있다.
(@patch("test_us.hello", return_value="Mock!")
와 같은 의미다)
@patch("__main__.hello", ...)
@patch()
데코레이터를 사용해서 patching 을 하면 Mock
객체를 테스트 메서드의 인자로 추가되는데 바로 mock_hello
이 이 Mock
객체의 매개변수 명으로 쓰이고 있다.
def test_hello(self, mock_hello):
테스트 메서드에서 검증하는 내용을 보면 hello()
함수를 호출했을 때 원래 리턴 값인 "Hello!"
대신에 "Mock!"
을 리턴하는지 검사한다.
self.assertEqual(hello(), "Mock!")
그리고 정말로 hello()
함수가 mock_hello()
함수로 대체가 되었는지 확인한다.
self.assertIs(hello, mock_hello)
그리고 Mock
객체에 함수 호출이 기억되었는지를 검증하고 있다.
mock_hello.assert_called_once_with()
@patch()
데코레이터는 외부 서비스에 의존하는 코드에 대한 테스트를 작성할 때 유용하게 쓰인다.
예를 들어, API 를 호출하는 코드에 대한 테스트를 작성할 때, 실제로 네트워크 연동을 하면 테스트가 느려지고 깨지기 쉽다.
지금부터 아래와 같이 requests
패키지를 사용하여 외부 API 와 연동하여 사용자를 조회하거나 생성해주는 간단한 모듈에 대한 Unit Test
를 작성해보겠다.
user_manage.py
import requests
def get_user(id):
res = requests.get(f"<https://jsonplaceholder.typicode.com/users/{id}>")
if res.status_code != 200:
raise Exception("Failed to get a user.")
return res.json()
def create_user(user):
res = requests.post(f"<https://jsonplaceholder.typicode.com/users>", data=user)
if res.status_code != 201:
raise Exception("Failed to create a user.")
return res.json()
get_user()
함수에 대한 테스트를 작성해보겠다.
get_user()
함수는 인자로 넘어온 사용자 id를 이용해서 URL을 만든 후, 이 URL을 인자로 넘겨 requests
패키지의 get()
함수를 호출한다.
그리고 requests
패키지의 get()
함수의 리턴 객체의 json()
함수를 호출한 결과를 리턴한다.
실제 네트워크 연동이 발생하지 않는 Unit Test
를 작성하려면 requests
패키지의 get()
함수를 patching 해줘야 한다.
즉, requests.get()
함수를 Mock
객체로 교체하고, 그 Mock
객체가 어떻게 작동할지 설정한 다음, 실제로 Mock
객체 대상으로 예상했던 작업이 일어났는지 검증해야 한다.
from unittest import TestCase
from unittest.mock import patch
import user_manager
class TestUserManager(TestCase):
@patch("requests.get")
def test_get_user(self, mock_get):
res = mock_get.return_value
res.status_code = 200
res.json.return_value = {
"name": "Test User",
"email": "user@test.com",
}
user = user_manager.get_user(1)
self.assertEqual(user["name"], "Test User")
self.assertEqual(user["email"], "user@test.com")
mock_get.assert_called_once_with("<https://jsonplaceholder.typicode.com/users/1>")
테스트 함수에 @patch("requests.get")
데코레이터를 선언하면, mock_get
매개 변수에 교체된 Mock
객체가 할당된다.
@patch("requests.get")
def test_get_user(self, mock_get):
이제 mock_get
에 할당되어 있는 Mock
객체가 리턴할 객체의 status_code
속성을 200
으로 지정하고, json
메서드의 리턴 객체를 임의의 사용자로 지정한다.
res = mock_get.return_value
res.status_code = 200
res.json.return_value = {
"name": "Test User",
"email": "user@test.com",
}
다음으로 user_manager
모듈의 get_user()
함수를 호출하여, 내부적으로 requests
모듈의 get()
함수가 호출되게 한다.
user = user_manager.get_user(1)
그 다음, get_user()
함수를 호출 결과가 임의의 사용자에 담긴 내용과 일치하는지 확인한다.
self.assertEqual(user["name"], "Test User")
self.assertEqual(user["email"], "user@test.com")
마지막으로 Mock
객체가 get_user()
함수에 인자로 넘어간 사용자 id 를 포함하는 정확한 URL 로 호출이 되었는지 검증한다.
mock_get.assert_called_once_with("<https://jsonplaceholder.typicode.com/users/1>")
???? patching 은 requests.get
와 같은 것을 내가 만든 가짜 함수로 만들겠다! 라는 의미인 것 같다.
from unittest import TestCase
from unittest.mock import patch
import user_manager
class TestUserManager(TestCase):
@patch("requests.post")
def test_create_user(self, mock_post):
res = mock_post.return_value
res.status_code = 201
res.json.return_value = {"id": 99}
user = user_manager.create_user(
{"name": "Test User", "email": "user@test.com",}
)
self.assertEqual(user["id"], 99)
mock_post.assert_called_once_with(
"<https://jsonplaceholder.typicode.com/users>",
data={"name": "Test User", "email": "user@test.com",},
)
create_user()
함수도 위와 같이 비슷한 방식으로 Unit Test
를 작성할 수 있다.
requests
패키지의 post()
함수를 patching 하고 Mock
객체가 할당된 mock_post
를 같은 방식으로 활용하면 된다.