테스트는 빠르고, deterministic 이고, 격리되어야. 진짜 HTTP API 치는 함수, 또는 파일 읽거나 DB 쿼리하는 함수 — 테스트 시점에 그것 다 X — 느리고, flaky 하고, 부작용 가득. Mocking 이 그 외부 의존성을 말한 거 반환하는 stand-in 으로 교체. 테스트되는 함수가 차이 모름.
unittest.mock — Mock 와 patch
Mock() 이 모든 호출 기록 + 어떤 속성 접근도 받는 객체 생성. patch("module.path") 가 테스트 동안 다른 모듈 네임스페이스의 뭔가 교체 (with-block, decorator, 또는 context manager). 조합이 mocking 필요의 90% 처리.
patch path — 사용된-모듈, 정의된-모듈 X
가장 흔한 mocking 버그 — 테스트 대상 코드가 from requests import get; get(...) 인데 patch("requests.get"). import 후 테스트 대상 네임스페이스의 함수가 별 참조, 원래 모듈 patching 이 영향 X. 해결 — patch("my_module.get") — 정의된 곳 X, lookup 되는 곳 patch.
monkeypatch — pytest 의 가벼운 대안
pytest 의 monkeypatch fixture 가 테스트 끝 자동 정리 + 비슷한 능력 제공. monkeypatch.setattr(module, "attr", value) 가 뭔가 swap. monkeypatch.setenv("VAR", "value") 가 env var 설정. 흔한 패턴 — pytest 테스트엔 더 깔끔한 mock.
War Story: Over-mocking 이 진짜 안티 패턴. 모든 거 mock 하는 테스트가 코드 대신 mock 테스트 끝남. Pippa 의 stack 이 경계에서 mock — 네트워크, LLM API, 시간 함수 — 모듈 간 내부 호출은 X. 선 — 프로세스 *간* mock, 코드 *내* 신뢰.
Code
Mock — 기본·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 X
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
pytest 의 monkeypatch fixture·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")
호출 assert·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)]
모듈 weather.py 에 get_weather(city) 함수 — requests import + requests.get(url).json() 호출. test_weather.py — patch("weather.requests.get") 으로 응답 mock, fake JSON dict 반환. 확인 — (a) 테스트가 네트워크 호출 안 함, (b) requests.get 에 넘긴 URL assert 가능, (c) side_effect 통해 requests.RequestException 시뮬레이트. patch 대신 monkeypatch 사용 두 번째 테스트 추가 + 비교.
Progress
Progress is local-only — sign in to sync across devices.