mock — 모의 객체
mock은 테스트에서 외부 의존성(DB, HTTP, 파일 I/O)을 가짜 객체로 대체합니다. unittest.mock은 표준 라이브러리이고, pytest-mock은 더 편리한 인터페이스를 제공합니다.
MagicMock 기초
from unittest.mock import MagicMock, patch
# MagicMock: 모든 속성과 메서드를 자동 생성
mock_db = MagicMock()
# 반환값 지정
mock_db.find_user.return_value = {"id": 1, "name": "Alice"}
mock_db.count.return_value = 42
# 사용
user = mock_db.find_user(user_id=1)
assert user["name"] == "Alice"
assert mock_db.count() == 42
# 호출 여부 확인
mock_db.find_user.assert_called_once()
mock_db.find_user.assert_called_with(user_id=1)
mock_db.count.assert_called_once()
# 호출 횟수
assert mock_db.find_user.call_count == 1
# 예외 발생 시뮬레이션
mock_db.delete.side_effect = PermissionError("삭제 권한 없음")
import pytest
with pytest.raises(PermissionError, match="삭제 권한 없음"):
mock_db.delete(user_id=1)
patch — 특정 경로 대체
from unittest.mock import patch, MagicMock
import pytest
# 테스트 대상 코드
# user_service.py
def get_user_from_db(user_id: int) -> dict:
import database # 실제 DB 모듈 (테스트에서 mock 교체)
return database.query(f"SELECT * FROM users WHERE id={user_id}")
# @patch: 함수 실행 동안만 mock 대체
@patch("database.query")
def test_get_user(mock_query):
mock_query.return_value = {"id": 1, "name": "Alice"}
from user_service import get_user_from_db
user = get_user_from_db(1)
assert user["name"] == "Alice"
mock_query.assert_called_once_with("SELECT * FROM users WHERE id=1")
# context manager 방식
def test_get_user_context():
with patch("database.query") as mock_query:
mock_query.return_value = {"id": 2, "name": "Bob"}
# 테스트 코드...
# 여러 mock 동시 적용 (역순으로 인자 전달)
@patch("module.ClassB")
@patch("module.ClassA")
def test_multiple_mocks(mock_a, mock_b):
# mock_a = ClassA, mock_b = ClassB
mock_a.return_value.method.return_value = "from A"
mock_b.return_value.method.return_value = "from B"
assert mock_a.return_value.method() == "from A"
patch.object — 특정 객체의 메서드 교체
from unittest.mock import patch, MagicMock
import requests
class WeatherService:
def get_temperature(self, city: str) -> float:
response = requests.get(f"https://api.weather.com/{city}")
return response.json()["temp"]
def test_get_temperature():
service = WeatherService()
with patch.object(service, "get_temperature", return_value=25.5) as mock_method:
temp = service.get_temperature("Seoul")
assert temp == 25.5
mock_method.assert_called_once_with("Seoul")
# requests.get 전체를 mock
def test_http_request():
mock_response = MagicMock()
mock_response.json.return_value = {"temp": 22.0}
mock_response.status_code = 200
with patch("requests.get", return_value=mock_response):
service = WeatherService()
temp = service.get_temperature("Busan")
assert temp == 22.0
pytest-mock (mocker fixture)
# pip install pytest-mock
import pytest
class EmailService:
def send(self, to: str, subject: str, body: str) -> bool:
# 실제로는 SMTP 서버에 전송
import smtplib
...
return True
class UserService:
def __init__(self, email_service: EmailService):
self.email_service = email_service
def register(self, email: str, name: str) -> dict:
user = {"id": 1, "email": email, "name": name}
self.email_service.send(
to=email,
subject="환영합니다",
body=f"안녕하세요 {name}님",
)
return user
# mocker fixture (pytest-mock)
def test_register_sends_email(mocker):
mock_email = mocker.MagicMock(spec=EmailService)
mock_email.send.return_value = True
service = UserService(email_service=mock_email)
user = service.register("alice@example.com", "Alice")
assert user["name"] == "Alice"
mock_email.send.assert_called_once_with(
to="alice@example.com",
subject="환영합니다",
body="안녕하세요 Alice님",
)
# mocker.patch: 경로 기반 패치
def test_with_mocker_patch(mocker):
mocker.patch("smtplib.SMTP")
email = EmailService()
result = email.send("test@test.com", "Test", "Body")
# smtplib.SMTP가 mock으로 대체됨
# spy: 실제 함수 호출 + 호출 기록
def test_spy(mocker):
service = UserService(email_service=EmailService())
spy = mocker.spy(service, "register")
service.register("test@test.com", "Test")
spy.assert_called_once()
assert spy.call_count == 1
side_effect 활용
from unittest.mock import MagicMock
import pytest
# 순서대로 다른 값 반환
mock = MagicMock()
mock.side_effect = [1, 2, 3, StopIteration]
assert mock() == 1
assert mock() == 2
assert mock() == 3
with pytest.raises(StopIteration):
mock()
# 함수로 동작 구현
def smart_side_effect(url: str) -> dict:
if "error" in url:
raise ConnectionError("연결 실패")
return {"url": url, "status": 200}
mock_get = MagicMock(side_effect=smart_side_effect)
result = mock_get("https://example.com/api")
assert result["status"] == 200
with pytest.raises(ConnectionError):
mock_get("https://example.com/error")
# 재시도 로직 테스트
def fetch_with_retry(url: str, retries: int = 3) -> dict:
import requests
for attempt in range(retries):
try:
return requests.get(url).json()
except ConnectionError:
if attempt == retries - 1:
raise
raise RuntimeError("미도달")
def test_retry_logic(mocker):
mock_get = mocker.patch("requests.get")
mock_response = MagicMock()
mock_response.json.return_value = {"data": "ok"}
# 처음 2번 실패, 3번째 성공
mock_get.side_effect = [
ConnectionError(),
ConnectionError(),
mock_response,
]
result = fetch_with_retry("https://example.com")
assert result["data"] == "ok"
assert mock_get.call_count == 3
시간·환경 mock
from unittest.mock import patch
import datetime
import os
def get_current_year() -> int:
return datetime.datetime.now().year
def test_mock_datetime(mocker):
mock_now = mocker.patch("datetime.datetime")
mock_now.now.return_value = datetime.datetime(2024, 6, 15, 12, 0, 0)
assert get_current_year() == 2024
# 환경변수 mock
def get_config() -> dict:
return {
"api_key": os.environ.get("API_KEY", ""),
"debug": os.environ.get("DEBUG", "false") == "true",
}
def test_env_mock(monkeypatch):
monkeypatch.setenv("API_KEY", "test-key-123")
monkeypatch.setenv("DEBUG", "true")
config = get_config()
assert config["api_key"] == "test-key-123"
assert config["debug"] is True
# 파일 시스템 mock
def read_config_file(path: str) -> str:
with open(path) as f:
return f.read()
def test_file_mock(mocker):
mock_open = mocker.mock_open(read_data="key=value\ndebug=true")
mocker.patch("builtins.open", mock_open)
content = read_config_file("/etc/config.ini")
assert "key=value" in content
정리
| 도구 | 용도 |
|---|---|
MagicMock() | 자동 속성/메서드 생성 mock 객체 |
return_value | 반환값 지정 |
side_effect | 예외 발생 또는 동적 반환 |
@patch("경로") | 전역 객체/함수 교체 |
patch.object(obj, "method") | 특정 객체 메서드 교체 |
mocker.patch | pytest-mock 방식의 패치 |
mocker.spy | 실제 실행 + 호출 기록 |
assert_called_once_with(...) | 호출 검증 |
monkeypatch | 환경변수, 속성 임시 교체 |
mock은 외부 의존성을 격리해 테스트를 빠르고 예측 가능하게 만듭니다. 단, 과도한 mock은 실제 동작과 괴리를 만들 수 있으므로 적절히 사용하세요.