Python Mock

김민범·2025년 6월 24일

etc

목록 보기
2/3

Mock은 테스트 시 실제 객체나 함수 대신 사용하는 가짜 객체입니다. 외부 의존성을 격리하고 테스트의 신뢰성을 높이는 핵심 기법입니다.

1. Mock 기본 개념

Mock이 필요한 이유

# 문제가 있는 테스트 코드
def send_email(to_email, subject, body):
    # 실제로 이메일을 보냄 - 테스트할 때마다 이메일이 발송됨!
    smtp_server.send_mail(to_email, subject, body)
    return True

def process_order(order_data):
    # 실제 결제 처리 - 테스트할 때마다 돈이 빠짐!
    payment_result = payment_gateway.charge(order_data['amount'])
    
    if payment_result.success:
        send_email(order_data['email'], "주문 완료", "주문이 완료되었습니다")
        return {"status": "success"}
    return {"status": "failed"}

# 테스트하기 어려운 상황들:
# 1. 외부 API 호출 (네트워크 의존성)
# 2. 데이터베이스 연결
# 3. 파일 시스템 접근
# 4. 시간 의존적 코드
# 5. 랜덤 값 생성

Mock의 장점

from unittest.mock import Mock, patch

# Mock을 사용한 안전한 테스트
def test_process_order_success():
    order_data = {
        'amount': 100,
        'email': 'test@example.com'
    }
    
    # 실제 결제나 이메일 발송 없이 테스트 가능
    with patch('payment_gateway.charge') as mock_charge, \
         patch('send_email') as mock_send_email:
        
        # Mock 객체의 반환값 설정
        mock_charge.return_value.success = True
        
        result = process_order(order_data)
        
        # 결과 검증
        assert result["status"] == "success"
        
        # 함수가 올바른 인자로 호출되었는지 검증
        mock_charge.assert_called_once_with(100)
        mock_send_email.assert_called_once_with(
            'test@example.com', 
            "주문 완료", 
            "주문이 완료되었습니다"
        )

2. unittest.mock 모듈

Mock 객체 생성

from unittest.mock import Mock, MagicMock

# 기본 Mock 객체 생성
mock_obj = Mock()

# 속성과 메서드 동적 생성
print(mock_obj.some_attribute)  # <Mock name='mock.some_attribute' id='...'>
print(mock_obj.some_method())   # <Mock name='mock.some_method()' id='...'>

# 반환값 설정
mock_obj.some_method.return_value = "mocked result"
print(mock_obj.some_method())  # "mocked result"

# 속성값 설정
mock_obj.some_attribute = "mocked attribute"
print(mock_obj.some_attribute)  # "mocked attribute"

MagicMock vs Mock

from unittest.mock import Mock, MagicMock

# Mock은 매직 메서드를 지원하지 않음
mock = Mock()
# mock['key']  # TypeError 발생

# MagicMock은 매직 메서드를 자동으로 지원
magic_mock = MagicMock()
magic_mock['key'] = 'value'
print(magic_mock['key'])  # 'value'
print(len(magic_mock))    # 0 (기본값)

# 매직 메서드 커스터마이징
magic_mock.__len__.return_value = 5
print(len(magic_mock))    # 5

# 반복 가능한 객체로 만들기
magic_mock.__iter__.return_value = iter([1, 2, 3])
for item in magic_mock:
    print(item)  # 1, 2, 3 출력

3. Mock 객체 설정 방법

spec과 spec_set 사용

class RealClass:
    def real_method(self):
        return "real"
    
    real_attribute = "real_attr"

# spec 사용 - 실제 클래스의 인터페이스만 허용
mock_with_spec = Mock(spec=RealClass)
mock_with_spec.real_method()  # 정상 동작
# mock_with_spec.fake_method()  # AttributeError 발생

# spec_set 사용 - 더 엄격한 검증
mock_with_spec_set = Mock(spec_set=RealClass)
mock_with_spec_set.real_attribute = "new_value"  # 정상
# mock_with_spec_set.fake_attribute = "fake"  # AttributeError 발생

configure_mock 사용

mock = Mock()

# 여러 속성을 한번에 설정
mock.configure_mock(
    name="John",
    age=30,
    get_info=Mock(return_value="John is 30 years old")
)

print(mock.name)         # "John"
print(mock.age)          # 30
print(mock.get_info())   # "John is 30 years old"

side_effect 사용

from unittest.mock import Mock

# 예외 발생시키기
mock = Mock()
mock.side_effect = ValueError("Something went wrong")
# mock()  # ValueError 발생

# 함수로 동작 정의
def custom_side_effect(*args, **kwargs):
    if args[0] == "valid":
        return "success"
    else:
        raise ValueError("Invalid input")

mock.side_effect = custom_side_effect
print(mock("valid"))    # "success"
# mock("invalid")       # ValueError 발생

# 여러 번 호출시 다른 값 반환
mock.side_effect = ["first", "second", "third"]
print(mock())  # "first"
print(mock())  # "second"
print(mock())  # "third"
# mock()       # StopIteration 발생

4. patch 데코레이터

기본 patch 사용법

from unittest.mock import patch
import requests

def get_user_data(user_id):
    response = requests.get(f"https://api.example.com/users/{user_id}")
    return response.json()

# 데코레이터로 patch 사용
@patch('requests.get')
def test_get_user_data(mock_get):
    # Mock 응답 설정
    mock_response = Mock()
    mock_response.json.return_value = {"id": 1, "name": "John"}
    mock_get.return_value = mock_response
    
    result = get_user_data(1)
    
    assert result == {"id": 1, "name": "John"}
    mock_get.assert_called_once_with("https://api.example.com/users/1")

# 컨텍스트 매니저로 patch 사용
def test_get_user_data_context():
    with patch('requests.get') as mock_get:
        mock_response = Mock()
        mock_response.json.return_value = {"id": 1, "name": "John"}
        mock_get.return_value = mock_response
        
        result = get_user_data(1)
        
        assert result == {"id": 1, "name": "John"}

patch.object 사용

class DatabaseConnection:
    def query(self, sql):
        # 실제 데이터베이스 쿼리 실행
        pass

class UserService:
    def __init__(self, db_connection):
        self.db = db_connection
    
    def get_user_count(self):
        result = self.db.query("SELECT COUNT(*) FROM users")
        return result[0]

# 특정 객체의 메서드만 패치
@patch.object(DatabaseConnection, 'query')
def test_get_user_count(mock_query):
    mock_query.return_value = [42]
    
    db = DatabaseConnection()
    service = UserService(db)
    
    count = service.get_user_count()
    
    assert count == 42
    mock_query.assert_called_once_with("SELECT COUNT(*) FROM users")

여러 객체 동시 패치

class EmailService:
    def send(self, to, subject, body):
        pass

class SMSService:
    def send(self, to, message):
        pass

class NotificationService:
    def __init__(self, email_service, sms_service):
        self.email = email_service
        self.sms = sms_service
    
    def notify_user(self, user, message):
        self.email.send(user.email, "알림", message)
        self.sms.send(user.phone, message)

@patch('app.SMSService.send')
@patch('app.EmailService.send')
def test_notify_user(mock_email_send, mock_sms_send):
    # 주의: 데코레이터 순서와 파라미터 순서가 반대!
    user = Mock(email="test@example.com", phone="123-456-7890")
    
    email_service = EmailService()
    sms_service = SMSService()
    notification_service = NotificationService(email_service, sms_service)
    
    notification_service.notify_user(user, "테스트 메시지")
    
    mock_email_send.assert_called_once_with("test@example.com", "알림", "테스트 메시지")
    mock_sms_send.assert_called_once_with("123-456-7890", "테스트 메시지")

5. 고급 Mock 기법

PropertyMock 사용

from unittest.mock import PropertyMock, patch

class User:
    def __init__(self, name):
        self._name = name
    
    @property
    def name(self):
        return self._name.upper()

# 프로퍼티 모킹
with patch.object(User, 'name', new_callable=PropertyMock) as mock_name:
    mock_name.return_value = "MOCKED NAME"
    
    user = User("john")
    assert user.name == "MOCKED NAME"

create_autospec 사용

from unittest.mock import create_autospec

class Calculator:
    def add(self, a, b):
        return a + b
    
    def multiply(self, a, b):
        return a * b

# 실제 클래스의 시그니처를 자동으로 따르는 Mock 생성
mock_calc = create_autospec(Calculator)

# 올바른 호출
mock_calc.add(1, 2)

# 잘못된 호출 - TypeError 발생
# mock_calc.add(1)  # missing required argument
# mock_calc.add(1, 2, 3)  # too many arguments
# mock_calc.subtract(1, 2)  # no such method

Mock 체이닝

from unittest.mock import Mock

# 복잡한 객체 구조 모킹
mock_api = Mock()
mock_api.users.get.return_value.json.return_value = {"id": 1, "name": "John"}
mock_api.users.get.return_value.status_code = 200

# 사용
response = mock_api.users.get(1)
assert response.status_code == 200
assert response.json() == {"id": 1, "name": "John"}

6. Mock 검증 (Assertion)

호출 검증

from unittest.mock import Mock, call

mock = Mock()

# 함수 호출
mock.method("arg1", "arg2", keyword="value")
mock.method("arg3")

# 호출 횟수 검증
mock.method.assert_called()  # 최소 한 번 호출되었는지
mock.method.assert_called_once()  # 정확히 한 번 호출되었는지 (실패)

# 특정 인자로 호출되었는지 검증
mock.method.assert_called_with("arg3")  # 마지막 호출 검증
mock.method.assert_called_once_with("arg1", "arg2", keyword="value")  # 실패

# 호출되지 않았는지 검증
mock.other_method.assert_not_called()

# 호출 횟수 직접 확인
assert mock.method.call_count == 2

# 모든 호출 내역 확인
expected_calls = [
    call("arg1", "arg2", keyword="value"),
    call("arg3")
]
mock.method.assert_has_calls(expected_calls)

복잡한 호출 패턴 검증

from unittest.mock import Mock, call, ANY

mock = Mock()

# 여러 메서드 호출
mock.start()
mock.process("data1")
mock.process("data2")
mock.stop()

# 전체 호출 순서 검증
expected_calls = [
    call.start(),
    call.process("data1"),
    call.process("data2"),
    call.stop()
]
assert mock.mock_calls == expected_calls

# ANY를 사용한 유연한 검증
mock.save.assert_called_with(ANY, format="json")  # 첫 번째 인자는 무엇이든 OK

7. 실제 시나리오별 Mock 활용

데이터베이스 Mock

from unittest.mock import Mock, patch
import sqlite3

class UserRepository:
    def __init__(self, db_path):
        self.db_path = db_path
    
    def get_user_by_id(self, user_id):
        conn = sqlite3.connect(self.db_path)
        cursor = conn.cursor()
        cursor.execute("SELECT * FROM users WHERE id = ?", (user_id,))
        result = cursor.fetchone()
        conn.close()
        return result

# 데이터베이스 연결 모킹
@patch('sqlite3.connect')
def test_get_user_by_id(mock_connect):
    # Mock 객체 설정
    mock_conn = Mock()
    mock_cursor = Mock()
    mock_connect.return_value = mock_conn
    mock_conn.cursor.return_value = mock_cursor
    mock_cursor.fetchone.return_value = (1, "John", "john@example.com")
    
    repo = UserRepository("/fake/path/db.sqlite")
    result = repo.get_user_by_id(1)
    
    assert result == (1, "John", "john@example.com")
    mock_connect.assert_called_once_with("/fake/path/db.sqlite")
    mock_cursor.execute.assert_called_once_with("SELECT * FROM users WHERE id = ?", (1,))

HTTP 요청 Mock

import requests
from unittest.mock import Mock, patch

class WeatherService:
    def __init__(self, api_key):
        self.api_key = api_key
    
    def get_weather(self, city):
        url = f"https://api.weather.com/v1/weather"
        params = {"key": self.api_key, "city": city}
        
        response = requests.get(url, params=params)
        response.raise_for_status()
        
        return response.json()

@patch('requests.get')
def test_get_weather_success(mock_get):
    # 성공 응답 모킹
    mock_response = Mock()
    mock_response.json.return_value = {
        "city": "Seoul",
        "temperature": 25,
        "condition": "sunny"
    }
    mock_response.raise_for_status.return_value = None
    mock_get.return_value = mock_response
    
    service = WeatherService("fake_api_key")
    result = service.get_weather("Seoul")
    
    assert result["city"] == "Seoul"
    assert result["temperature"] == 25
    
    mock_get.assert_called_once_with(
        "https://api.weather.com/v1/weather",
        params={"key": "fake_api_key", "city": "Seoul"}
    )

@patch('requests.get')
def test_get_weather_http_error(mock_get):
    # HTTP 에러 모킹
    mock_response = Mock()
    mock_response.raise_for_status.side_effect = requests.HTTPError("404 Not Found")
    mock_get.return_value = mock_response
    
    service = WeatherService("fake_api_key")
    
    with pytest.raises(requests.HTTPError):
        service.get_weather("UnknownCity")

파일 시스템 Mock

from unittest.mock import mock_open, patch
import json

class ConfigManager:
    def __init__(self, config_path):
        self.config_path = config_path
    
    def load_config(self):
        with open(self.config_path, 'r') as f:
            return json.load(f)
    
    def save_config(self, config):
        with open(self.config_path, 'w') as f:
            json.dump(config, f)

@patch('builtins.open', new_callable=mock_open, read_data='{"debug": true, "port": 8080}')
def test_load_config(mock_file):
    manager = ConfigManager("/fake/path/config.json")
    config = manager.load_config()
    
    assert config["debug"] is True
    assert config["port"] == 8080
    
    mock_file.assert_called_once_with("/fake/path/config.json", 'r')

@patch('builtins.open', new_callable=mock_open)
@patch('json.dump')
def test_save_config(mock_json_dump, mock_file):
    config_data = {"debug": False, "port": 9000}
    
    manager = ConfigManager("/fake/path/config.json")
    manager.save_config(config_data)
    
    mock_file.assert_called_once_with("/fake/path/config.json", 'w')
    mock_json_dump.assert_called_once_with(config_data, mock_file.return_value.__enter__.return_value)

시간 의존성 Mock

from unittest.mock import patch
from datetime import datetime, timedelta

class TaskScheduler:
    def is_business_hours(self):
        now = datetime.now()
        return 9 <= now.hour <= 17 and now.weekday() < 5  # 월-금 9-17시
    
    def get_next_business_day(self):
        now = datetime.now()
        if now.weekday() == 4:  # 금요일
            return now + timedelta(days=3)  # 월요일
        elif now.weekday() == 5:  # 토요일
            return now + timedelta(days=2)  # 월요일
        else:
            return now + timedelta(days=1)

@patch('datetime.datetime')
def test_is_business_hours_weekday(mock_datetime):
    # 화요일 오후 2시로 설정
    mock_datetime.now.return_value = datetime(2023, 5, 16, 14, 0)
    
    scheduler = TaskScheduler()
    assert scheduler.is_business_hours() is True

@patch('datetime.datetime')
def test_is_business_hours_weekend(mock_datetime):
    # 토요일 오후 2시로 설정
    mock_datetime.now.return_value = datetime(2023, 5, 13, 14, 0)
    
    scheduler = TaskScheduler()
    assert scheduler.is_business_hours() is False

@patch('datetime.datetime')
def test_get_next_business_day_friday(mock_datetime):
    # 금요일로 설정
    friday = datetime(2023, 5, 19, 14, 0)
    mock_datetime.now.return_value = friday
    
    scheduler = TaskScheduler()
    next_day = scheduler.get_next_business_day()
    
    # 다음 월요일이어야 함
    assert next_day.weekday() == 0  # 월요일
    assert next_day.day == 22

8. Mock의 함정과 주의사항

과도한 Mock 사용

# 나쁜 예 - 모든 것을 Mock
@patch('datetime.datetime')
@patch('random.random')
@patch('os.path.exists')
@patch('builtins.open')
def test_complex_function(mock_open, mock_exists, mock_random, mock_datetime):
    # 너무 많은 Mock은 테스트를 복잡하게 만들고 유지보수를 어렵게 함
    pass

# 좋은 예 - 핵심 외부 의존성만 Mock
def test_complex_function():
    # 실제 의존성을 주입받는 구조로 설계하여 Mock 최소화
    mock_external_service = Mock()
    service = MyService(external_service=mock_external_service)
    # 테스트 진행...

Mock의 잘못된 설정

from unittest.mock import Mock

# 잘못된 Mock 설정
mock = Mock()
mock.method.return_value = "result"

# 실수: return_value 대신 side_effect를 설정해야 하는 경우
def some_function():
    mock.method()  # 이 함수는 예외를 발생시켜야 함

# 올바른 설정
mock.method.side_effect = ValueError("Something went wrong")

Mock 검증 누락

@patch('external_api.call')
def test_without_proper_verification(mock_call):
    mock_call.return_value = {"status": "success"}
    
    result = my_function()
    
    # 잘못된 테스트 - Mock이 호출되었는지 검증하지 않음
    assert result == "expected"

@patch('external_api.call')
def test_with_proper_verification(mock_call):
    mock_call.return_value = {"status": "success"}
    
    result = my_function()
    
    # 올바른 테스트 - Mock 호출 검증 포함
    assert result == "expected"
    mock_call.assert_called_once_with(expected_argument)

9. Mock과 FastAPI 통합

FastAPI 의존성 Mock

from fastapi.testclient import TestClient
from unittest.mock import Mock
import pytest

# app/dependencies.py
class DatabaseService:
    def get_user(self, user_id: int):
        # 실제 데이터베이스 조회
        pass

# app/main.py
from fastapi import FastAPI, Depends

app = FastAPI()

def get_database_service():
    return DatabaseService()

@app.get("/users/{user_id}")
async def get_user(user_id: int, db_service: DatabaseService = Depends(get_database_service)):
    user = db_service.get_user(user_id)
    return user

# tests/test_main.py
def test_get_user():
    # Mock 의존성 생성
    mock_db_service = Mock()
    mock_db_service.get_user.return_value = {"id": 1, "name": "John"}
    
    # 의존성 오버라이드
    app.dependency_overrides[get_database_service] = lambda: mock_db_service
    
    client = TestClient(app)
    response = client.get("/users/1")
    
    assert response.status_code == 200
    assert response.json() == {"id": 1, "name": "John"}
    
    # Mock 호출 검증
    mock_db_service.get_user.assert_called_once_with(1)
    
    # 테스트 후 정리
    app.dependency_overrides.clear()

비동기 함수 Mock

import asyncio
from unittest.mock import AsyncMock, patch

class AsyncExternalService:
    async def fetch_data(self, url):
        # 실제 비동기 HTTP 요청
        pass

async def process_data():
    service = AsyncExternalService()
    data = await service.fetch_data("https://api.example.com/data")
    return data

@patch.object(AsyncExternalService, 'fetch_data', new_callable=AsyncMock)
async def test_process_data(mock_fetch):
    mock_fetch.return_value = {"data": "mocked"}
    
    result = await process_data()
    
    assert result == {"data": "mocked"}
    mock_fetch.assert_called_once_with("https://api.example.com/data")

# pytest에서 실행
@pytest.mark.asyncio
async def test_async_function():
    await test_process_data()

Mock을 효과적으로 사용하면 외부 의존성 없이 안정적이고 빠른 테스트를 작성할 수 있습니다. 핵심은 적절한 수준의 Mock 사용철저한 검증입니다!

0개의 댓글