Logo

[파이썬] 테스트 모킹 - patch

지난 포스팅에서 unittest.mock 모듈의 Mock 또는 MagicMock 클래스를 사용하여 기본적인 mocking을 하는 방법에 대해서 알아보았습니다. 이번 포스팅에서는 실전에서 더 많이 쓰이는 unittest.mock 모듈의 patch() 데코레이터를 이용하여 단위 테스트를 좀 더 세련되게 작성하는 방법에 대해서 알아보겠습니다.

patching? mocking?

unittest.mock 모듈의 patch() 데코레이터를 이용하면 특정 모듈의 함수나 클래스를 가짜(mock) 객체, 좀 더 엄밀히 말하면, MagicMock 인스턴스로 대체할 수 있습니다. 이 과정을 흔히 mocking또는 patching이라고 하는데, 단위 테스트를 작성할 때 외부 서비스에 의존하지 않고 독립적으로 실행이 가능한 단위 테스트를 작성하기 위해서 사용되는 테스팅 기법입니다.

patch 데코레이터

unittest.mock 모듈의 patch() 데코레이터는 특정 범위 내에서만 mocking이 가능하도록 해주는데요. 일반적으로 다음과 같이 patching이 필요한 단위 테스트 메서드에 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()
$ python test_me.py
.
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

위 예제를 보면, 원래 "Hello!"을 리턴하는 hello() 함수가 "Mock!"를 대신 리턴하도록 @patch() 데코레이터로 patching을 하고 있습니다.

@patch() 데코레이터는 첫번째 인자로 patching할 메서드를 package.module.Class.method 형태의 문자열로 받습니다. 본 예제에서는 patching할 메서드가 같은 모듈에 있기 때문에 __main__ 모듈명을 사용하고 있습니다. @patch() 데코레이터를 사용해서 patching을 하면 mock 객체를 테스트 메서드의 인자로 추가되는데, 바로 mock_hello이 이 mock 객체의 매개변수 명으로 쓰이고 있습니다.

테스트 메서드에서 검증하는 내용을 보면 hello() 함수를 호출했을 때 원래 리턴 값인 "Hello!" 대신에 "Mock!"을 리턴하는지 검사합니다. 그리고 정말로 hello() 함수가 mock_hello() 함수로 대체가 되었는지, 그리고 mock 객체에 함수 호출이 기억되었는지를 검증하고 있습니다.

실전 예제

@patch() 데코레이터는 외부 서비스에 의존하는 코드에 대한 테스트를 작성할 때 유용하게 쓰입니다. 예를 들어, API를 호출하는 코드에 대한 테스트를 작성할 때, 실제로 네트워크 연동을 하면 테스트가 느려지고, 깨지기 쉬워집니다.

지금부터 아래와 같이, requests 패키지를 사용하여 외부 API와 연동하여 사용자를 조회하거나 생성해주는 간단한 모듈에 대한 단위 테스트를 작성해보겠습니다.

  • user_manage.py
import requests


def get_user(id):
    response = requests.get(f"https://jsonplaceholder.typicode.com/users/{id}")
    if response.status_code != 200:
        raise Exception("Failed to get a user.")
    return response.json()


def create_user(user):
    response = requests.post(f"https://jsonplaceholder.typicode.com/users", data=user)
    if response.status_code != 201:
        raise Exception("Failed to create a user.")
    return response.json()

먼저 사용자 ID를 받아서, 사용자를 조회해주는 get_user() 함수에 대한 테스트를 작성해보겠습니다. get_user() 함수는 인자로 넘어온 사용자 ID를 이용해서 URL을 만든 후, 이 URL을 인자로 넘겨 requests 패키지의 get() 함수를 호출합니다. 그리고 requests 패키지의 get() 함수의 리턴 객체의 json() 함수를 호출한 결과를 리턴합니다.

실제 네트워크 연동이 발생하지 않는 단위 테스트를 작성하려면 requests 패키지의 get() 함수를 patching해줘야 합니다. 즉, requests.get() 함수를 mock 객체로 교체하고, 그 mock 객체가 어떻게 작동할지 설정한 다음, 실제로 mock 객체 대상으로 예상했던 작업이 일어났는지 검증해야 합니다.

from unittest import TestCase
from unittest.mock import patch

import user_manager


class TestUserManger(TestCase):
    @patch("requests.get")
    def test_get_user(self, mock_get):
        response = mock_get.return_value
        response.status_code = 200
        response.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 객체가 할당됩니다. 이제 mock_get에 할당되어 있는 mock 객체가 리턴할 객체의 status_code 속성을 200으로 지정하고, json 메서드의 리턴 객체를 임의의 사용자 사전을 지정합니다.

다음으로, user_manager 모듈의 get_user() 함수를 호출하여, 내부적으로 requests 모듈의 get() 함수가 호출되게 합니다.

그 다음, get_user() 함수를 호출 결과가 임의의 사용자 사전에 담긴 내용과 일치하는 확인합니다. 마지막으로 mock 객체가 get_user() 함수에 인자로 넘어간 사용자 ID를 포함하는 정확한 URL로 호출이 되었는지 검증합니다.

from unittest import TestCase
from unittest.mock import patch

import user_manager


class TestUserManger(TestCase):
    @patch("requests.post")
    def test_create_user(self, mock_post):
        response = mock_post.return_value
        response.status_code = 201
        response.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() 함수도 위와 같이 비슷한 방식으로 단위 테스트를 작성할 수 있습니다. requests 패키지의 post() 함수를 patching하고 mock 객체가 할당된 mock_post를 같은 방식으로 활용하면 됩니다.

마치면서

이상으로 unittest.mock 모듈의 @patch() 데코레이터를 이용하여 실전에서 단위 테스트가 이뤄지는 살펴보았습니다.