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. 랜덤 값 생성
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',
"주문 완료",
"주문이 완료되었습니다"
)
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"
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 출력
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 발생
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"
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 발생
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"}
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", "테스트 메시지")
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"
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
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"}
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
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,))
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")
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)
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
# 나쁜 예 - 모든 것을 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)
# 테스트 진행...
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")
@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)
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()
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 사용과 철저한 검증입니다!