본문 바로가기
C.W.K.
Stream
Lesson 02 of 05 · published

목 — 바깥세상만 가짜로 세우기

~18 min · mock, patch, monkeypatch, stub

Level 0호기심
0 XP0/93 lessons0/23 achievements
0/100 XP to next level100 XP to go0% complete

네트워크·파일·DB·시간 같은 외부 경계는 느리거나 흔들리고 부작용도 있어. 목은 테스트 동안 그 경계를 정해 둔 응답으로 바꾸고 호출을 기록해, 대상 함수의 관찰 가능한 행동만 빠르고 결정적으로 검사하게 해.

Mock()은 호출과 속성 접근을 기록하고, patch는 대상 코드가 실행되는 동안 이름을 바꿔. 핵심은 정의된 곳이 아니라 조회하는 곳을 바꾸는 거야. from requests import get을 실행한 my_modulerequests.get이 아니라 my_module.get을 바꿔야 해. pytest의 monkeypatchsetattr·setenv와 자동 정리를 제공해.

내부 호출 순서를 모조리 흉내 내면 구현을 시험하는 목 시험이 돼. 프로세스 밖의 계약과 실패만 가짜로 만들고, 코드 안의 작은 협력 객체는 실제로 돌리는 편이 리팩터링에도 강해.

Code

목의 반환값과 호출 기록·python
from unittest.mock import Mock

# mock 생성 — 어떤 호출도 받음, Mock 반환
m = Mock()
m.do_something("foo", count=5)
m.other.deep.call()

# 일어난 거 검사
print(m.do_something.called)               # True
print(m.do_something.call_args)            # call('foo', count=5)
print(m.do_something.call_count)           # 1

# 반환값 설정
m.compute.return_value = 42
print(m.compute())                          # 42
print(m.compute("any", "args"))            # 42 — 인자 무시

# Side effect — raise 또는 순서 반환
m.flaky.side_effect = [1, 2, ValueError("oops")]
print(m.flaky())                           # 1
print(m.flaky())                           # 2
try:
    m.flaky()
except ValueError:
    print("세 번째에 raise")
조회되는 이름을 patch하기·python
# my_module.py
# import requests
# def fetch_user(uid):
#     resp = requests.get(f"https://api.example.com/{uid}")
#     return resp.json()

# test_my_module.py
from unittest.mock import patch, Mock

def test_fetch_user():
    fake_resp = Mock()
    fake_resp.json.return_value = {"name": "alice", "id": 42}

    # 사용된 곳 patch — my_module.requests, requests가 아님
    with patch("my_module.requests.get", return_value=fake_resp) as fake_get:
        # from my_module import fetch_user
        # result = fetch_user(42)
        # assert result == {"name": "alice", "id": 42}
        # fake_get.assert_called_once_with("https://api.example.com/42")
        pass
patch 데코레이터로 테스트 감싸기·python
from unittest.mock import patch, Mock

@patch("my_module.requests.get")
def test_with_decorator(mock_get):
    mock_get.return_value.json.return_value = {"x": 1}
    # ... my_module.fetch 호출 ...
    # ... mock_get.called assert ...

# 여러 patch 스택 — 파라미터 list 첫 번째가 가장 안쪽
@patch("my_module.B")
@patch("my_module.A")
def test_stacked(mock_A, mock_B):
    # mock_A 가 안의 @patch, 첫 list
    # mock_B 가 바깥 @patch, 두 번째 list
    pass
monkeypatch로 속성과 환경 바꾸기·python
# pytest 안에서 patch 보다 깔끔한 대안

def test_with_monkeypatch(monkeypatch):
    # 속성 교체
    import my_module
    fake_get = lambda url: {"fake": True}
    monkeypatch.setattr(my_module, "fetch", fake_get)
    # ... 테스트 코드 ...
    # 테스트 끝 자동 정리

    # env var 설정 (자동 복원)
    monkeypatch.setenv("DEBUG", "true")
    # ... 테스트 코드 ...

    # 디렉토리 변경 (자동 복원)
    monkeypatch.chdir("/tmp")
관찰 가능한 호출만 확인하기·python
from unittest.mock import Mock

m = Mock()
m.send("hello", priority=1)
m.send("world", priority=5)

# 다양한 assertion
m.send.assert_called()                  # 최소 한 번
m.send.assert_called_once()             # 정확히 한 번 — 두 번 호출되면 실패
m.send.assert_called_with("world", priority=5)   # 마지막 호출 매치
m.send.assert_any_call("hello", priority=1)      # 어떤 호출 매치

# 전체 history 검사
print(m.send.call_args_list)
# [call('hello', priority=1), call('world', priority=5)]

External links

Exercise

weather.pyget_weather(city)requests.get(url).json()을 호출하게 만들어. weather.requests.get을 바꿔 네트워크 없이 가짜 JSON을 돌려주고 URL을 확인한 뒤 side_effectRequestException도 재현해. 같은 경계를 monkeypatch로 바꾼 두 번째 테스트와 비교해.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.