본문으로 건너뛰기
Advertisement

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.patchpytest-mock 방식의 패치
mocker.spy실제 실행 + 호출 기록
assert_called_once_with(...)호출 검증
monkeypatch환경변수, 속성 임시 교체

mock은 외부 의존성을 격리해 테스트를 빠르고 예측 가능하게 만듭니다. 단, 과도한 mock은 실제 동작과 괴리를 만들 수 있으므로 적절히 사용하세요.

Advertisement