[Unit Test] unittest mock이란?(mock, magicmock, patch)

Unit Test를 진행하다 보면, 데이터베이스 또는 외부 API에 의존하는 코드를 테스트해야 할 일이 필연적으로 생긴다.

이 경우에 외부 API를 호출하기는 불가능한 경우가 많다. 이러한 상황에서 단위 테스트를 작성할 때, 외부의 의존하는 부분을 가짜로 대체하는 기법이 자주 사용된다. 이를 모킹(Mocking)이라고 한다.

image.png

unittest.mock이란?

python 표준 라이브러리인 unittest 모듈에 포함된 라이브러리로, 외부의 의존하는 부분을 가짜로 대체할때 사용된다.

사용법

Mock 클래스 사용

1. 반환값 설정

from unittest.mock import Mock

mock_obj = Mock()

mock_obj.some_method.return_value = 42

result = mock_obj.some_method()

print(result)
>>>
42

2. 호출 횟수 검증

from unittest.mock import Mock

mock_obj = Mock()

mock_obj.some_method()
mock_obj.some_method()

print(mock_obj.some_method.call_count)
>>>
2

3. 전달된 인자 검증

from unittest.mock import Mock

mock_obj = Mock()
mock_obj.some_method(5, 10)
mock_obj.some_method.assert_called_with(5, 10)

4. 연속된 반환값 설정

from unittest.mock import Mock

mock_obj = Mock()

# mock_obj.some_method가 호출될 때마다 1, 2, 3을 순차적으로 반환하도록 설정
mock_obj.some_method.side_effect = [1, 2, 3]

print(mock_obj.some_method())
print(mock_obj.some_method())
print(mock_obj.some_method())
>>>
1
2
3

5. 예외 발생시키기

from unittest.mock import Mock

mock_obj = Mock()

mock_obj.some_method.side_effect = ValueError("Uncorected Value")

try:
    mock_obj.some_method()
except ValueError as e:
    print(f"raise execption: {e}")
>>>
raise execption: Uncorected Value

6. property 설정

from unittest.mock import Mock

mock_obj = Mock()
mock_obj.some_property = "Hello, Mock!"
print(mock_obj.some_property)  # Hello, Mock!

7. Mock객체의 method의 호출 여부와 전달인자 검사

from unittest.mock import Mock

mock_obj = Mock()
mock_obj.some_method(1, 2)

mock_obj.some_method.assert_called_once_with(1, 2)

객체가 한번 호출되었는지와 전달된 인자가 적절한지 검사한다.


MagicMock 클래스 사용법

Unit Test를 진행하다 보면, Mock 클래스 외에도 더 복잡한 객체나 동작을 모킹할 필요가 생긴다. 이때 유용한 것이 바로 MagicMock이다. MagicMock은 Mock 클래스를 확장한 클래스인데, Magic Method(예: getitem, call, iter, len 등)를 포함한 다양한 기능을 지원한다. 이 덕분에 더 복잡한 테스트 시나리오를 다룰 수 있다.

1. getitem, setitem모킹(Dictionary와 유사)

from unittest.mock import MagicMock

mock_dict = MagicMock()

mock_dict['name'] = 'John'

print(mock_dict['name'])  # John

print(mock_dict.__setitem__.call_args)
>>>
call('name', 'John')

2. iter 모킹

iterator로 만들어서 for문을 사용하게 만든다.

from unittest.mock import MagicMock
mock_list = MagicMock()
mock_list.__iter__.return_value = iter([1, 2, 3])
for item in mock_list:
    print(item)
>>>
1
2
3

3. call 모킹

객체가 함수처럼 호출되도록 만들 수 있다.

from unittest.mock import MagicMock

mock_function = MagicMock()
mock_function(10, 20)
mock_function.assert_called_with(10, 20)

4. len 모킹

from unittest.mock import MagicMock

mock_obj = MagicMock()
mock_obj.__len__.return_value = 5
print(len(mock_obj))
>>>
5

5. enterexit 모킹

from unittest.mock import MagicMock

mock_context = MagicMock()

mock_context.__enter__.return_value = 'Resource'
mock_context.__exit__.return_value = None

with mock_context as resource:
    print(resource)
>>>
Resource

Patch사용법

patch는 특정 객체나 함수, 메서드를 테스트하는 동안 일시적으로 다른 객체나 값으로 대체할 수 있게 해준다. 이 기능은 특정 코드나 의존성이 복잡하거나 외부와 연결되어 있는 경우, 해당 의존성을 모킹하여 독립적인 단위 테스트를 작성할 수 있게 도와준다.

patch를 사용하면 테스트 대상 코드 내에서 특정 함수나 객체를 “대체”할 수 있으며, 이를 통해 테스트 환경을 설정하고, 실제 함수나 외부 시스템에 영향을 미치지 않고 테스트를 수행할 수 있다.

patch는 크게 두 가지 방식으로 사용할 수 있다:

  1. 데코레이터 방식
  2. 컨텍스트 매니저 방식

1. patch 데코레이터 방식

from unittest.mock import patch

# 대체할 함수
def get_data_from_api():
    # 실제 API 호출 코드 (외부 의존성)
    return "Real data from API"

# 테스트 함수에서 해당 함수 호출
@patch('__main__.get_data_from_api', return_value="Mocked data")
def test_get_data(mocked_get_data):
    result = get_data_from_api()
    print(result)
    mocked_get_data.assert_called_once()
>>>
Mocked data

2. patch Context Manager 방식

patch는 with 구문을 통해도 사용할 수 있다. 이 방식은 특정 테스트 코드 내에서만 일시적으로 값을 변경하고, with 블록이 끝나면 자동으로 원래 상태로 복원된다.

from unittest.mock import patch

def get_data_from_api():
    ## api내용
    return "Real data from API"

def test_get_data():
    with patch('__main__.get_data_from_api', return_value="Mocked data"):
        result = get_data_from_api()
        print(result)

    result = get_data_from_api()
    print(result)
>>>
Mocked data
Real data from API

patch가 적용된 범위는 with 블록 내부로 한정된다. 블록을 벗어나면 원래의 함수나 객체로 돌아간다.

3. patch의 주요 사용법

1) 클래스 속성 모킹 - Context Manager 사용

from unittest.mock import patch

class MyClass:
    def get_data(self):
        return "Real data"

def test_class_method():
    with patch.object(MyClass, 'get_data', return_value="Mocked data"):
        obj = MyClass()
        result = obj.get_data()
        print(result)

    obj = MyClass()
    print(obj.get_data())
>>>
Mocked data
Real data

2) 모듈 내 함수 모킹

import requests
from unittest.mock import patch

def fetch_data():
    response = requests.get("https://api.example.com/data")
    return response.text

def test_fetch_data():
    with patch('requests.get', return_value="Mocked API Response"):
        result = fetch_data()
        print(result)
>>>
Mocked API Response

위 코드에서는 requests.get을 모킹하여 실제 API 호출을 방지하고, fetch_data 함수가 “Mocked API Response”를 반환하도록 했다.

3) patch의 side_effect 사용(함수 바꿔치기)

patch는 side_effect를 설정하여 함수 호출 시 반환값을 동적으로 변경할 수 있다.

from unittest.mock import patch

def fetch_data_from_db():
    # 실제 DB 호출 코드
    return "Real data from DB"

def test_fetch_data_with_side_effect():
    def side_effect_function(*args, **kwargs):
        if args[0] == "user1":
            return "Mocked data for user1"
        else:
            raise ValueError("Invalid user")

    with patch('__main__.fetch_data_from_db', side_effect=side_effect_function):
        print(fetch_data_from_db("user1"))
        try:
            print(fetch_data_from_db("user2"))
        except ValueError as e:
            print(f"Caught exception: {e}")
>>>
Mocked data for user1
Caught exception: Invalid user

side_effect를 사용하여 fetch_data_from_db 함수 호출 시 조건에 따라 다른 값을 반환하거나 예외를 발생시킬 수 있다.

4) patch의 여러 인자 모킹

from unittest.mock import patch

def test_multiple_patches():
    with patch('__main__.get_data_from_api', return_value="Mocked data") as mock_api, \
         patch('__main__.get_data_from_db', return_value="Mocked DB data") as mock_db:

        api_result = get_data_from_api()
        db_result = get_data_from_db()
        print(api_result)
        print(db_result)
        mock_api.assert_called_once()
        mock_db.assert_called_once()
>>>
Mocked data
Mocked DB data

patch는 동시에 여러 개의 객체나 함수를 모킹할 수 있으며, 각 모킹된 객체에 대해서 별도로 assert_called_once 등의 검증도 할 수 있다.

참조문서 및 그림

https://kr.mathworks.com/help/matlab/mock-dependencies-in-tests.html?lang=en