-
[Python] pytest, pytest-mock 라이브러리언어/파이썬 & 장고 2026. 4. 12. 22:01
1. 개요
pytest와 pytest-mock은 Python 생태계에서 가장 널리 사용되는 테스트 관련 라이브러리입니다. 두 라이브러리는 함께 사용하도록 설계되어 있으며, 단위 테스트(Unit Test)부터 통합 테스트(Integration Test)까지 다양한 테스트 시나리오를 깔끔하게 작성할 수 있도록 도와줍니다.
구분 pytest pytest-mock 역할 테스트 프레임워크 (실행, 발견, 보고) 모킹(Mocking) 지원 플러그인 핵심 개념 Fixture, Marker, Parametrize mocker fixture, patch, spy 독립 사용 가능 pytest 의존 필수
2. 라이브러리 기능 설명
2-1. pytest
pytest는 Python의 표준
unittest보다 훨씬 간결하고 강력한 테스트 프레임워크입니다. 보일러플레이트 코드 없이 일반 함수로 테스트를 작성할 수 있으며, 풍부한 플러그인 생태계를 보유하고 있습니다.핵심 기능
- Fixture — 테스트 의존성 및 환경 설정 관리
Fixture는 scope 설정으로 생명주기를 제어할 수 있습니다.import pytest @pytest.fixture def user(): return {"id": 1, "name": "홍길동", "email": "hong@example.com"} def test_user_name(user): assert user["name"] == "홍길동" @pytest.fixture(scope="module") # function, class, module, session def db_connection(): conn = create_connection() yield conn # yield 이후는 teardown 로직 conn.close()- Fixture는 테스트 함수에 주입되는 재사용 가능한 설정 단위입니다. 데이터베이스 연결, 임시 파일, 공통 객체 등을 깔끔하게 관리할 수 있습니다.
- Parametrize — 여러 입력값으로 동일한 테스트 반복 실행
import pytest @pytest.mark.parametrize("input_val, expected", [ (1, 2), (10, 11), (-1, 0), (0, 1), ]) def test_increment(input_val, expected): assert input_val + 1 == expected @pytest.mark.parametrize데코레이터를 사용하면 하나의 테스트 함수로 다양한 케이스를 검증할 수 있습니다.- Markers — 테스트 분류 및 조건부 실행
실행 시 마커로 필터링할 수 있습니다.import pytest import sys @pytest.mark.skip(reason="아직 구현 중") def test_not_ready(): pass @pytest.mark.skipif(sys.platform == "win32", reason="Windows 미지원") def test_unix_only(): pass @pytest.mark.xfail(reason="알려진 버그 #123") def test_known_bug(): assert 1 == 2 # 커스텀 마커 등록 (pytest.ini 또는 pyproject.toml) @pytest.mark.slow def test_heavy_computation(): pass pytest -m slow # slow 마커만 실행 pytest -m "not slow" # slow 제외 실행- 마커를 통해 테스트를 그룹화하거나 특정 조건에서만 실행할 수 있습니다.
- Assert 인트로스펙션 — 실패 시 상세한 차이 출력
def test_list_contains(): result = [1, 2, 3, 4] expected = [1, 2, 3, 5] assert result == expected # 어떤 원소가 다른지 명확히 출력됨 assert문만 사용해도 pytest가 실패 원인을 상세히 출력해 줍니다.assertEqual,assertIn등 별도 메서드가 필요 없습니다.- 예외 검증 — 특정 예외 발생 확인
import pytest def divide(a, b): if b == 0: raise ZeroDivisionError("0으로 나눌 수 없습니다.") return a / b def test_divide_by_zero(): with pytest.raises(ZeroDivisionError, match="0으로 나눌 수 없습니다"): divide(10, 0)- tmp_path / conftest.py — 내장 Fixture와 전역 설정
tmp_path— 테스트별 임시 디렉토리capsys— stdout/stderr 캡처monkeypatch— 환경변수, 속성, 딕셔너리 임시 변경caplog— 로그 캡처conftest.py에 정의한 fixture는 동일 디렉토리 및 하위 디렉토리 전체에서 자동으로 공유됩니다.
- pytest는 다양한 내장 fixture를 제공합니다.
2-2. pytest-mock
pytest-mock은 Python 표준 라이브러리인
unittest.mock을 pytest의 fixture 방식으로 사용할 수 있게 해주는 얇은 래퍼(thin wrapper) 플러그인입니다.mockerfixture 하나로 mock, patch, spy 등 모든 모킹 기능을 간결하게 사용할 수 있습니다.핵심 기능
- mocker.patch — 객체/함수를 가짜(Mock)로 대체반환값 설정:
def test_get_user(mocker): mocker.patch("myapp.db.get_user", return_value={"id": 1, "name": "홍길동"}) result = myapp.db.get_user(1) assert result["name"] == "홍길동"import os def test_remove_file(mocker): mocker.patch("os.remove") # os.remove를 Mock으로 대체 os.remove("fake_file.txt") os.remove.assert_called_once_with("fake_file.txt")- mocker.patch.object — 특정 객체의 메서드 패치
def test_service(mocker): service = MyService() mocker.patch.object(service, "fetch_data", return_value=[1, 2, 3]) result = service.process() assert result == [1, 2, 3]- mocker.spy — 실제 동작을 유지하면서 호출 추적
def test_spy_method(mocker): class Calculator: def add(self, a, b): return a + b calc = Calculator() spy = mocker.spy(calc, "add") result = calc.add(3, 4) assert result == 7 # 실제 로직 실행됨 spy.assert_called_once_with(3, 4) # 호출 검증 assert spy.spy_return == 7 # 반환값 검증 spy는 원본 함수를 그대로 실행하면서 호출 여부, 인자, 반환값을 기록합니다. patch와 달리 실제 로직이 실행됩니다.- Mock 클래스 직접 접근 — MagicMock, AsyncMock 등
def test_mock_classes(mocker): # MagicMock 생성 mock_repo = mocker.MagicMock() mock_repo.find.return_value = [{"id": 1}] # AsyncMock (비동기 함수 모킹) mock_async = mocker.AsyncMock(return_value="async result") # mock_open (파일 읽기 모킹) m = mocker.patch("builtins.open", mocker.mock_open(read_data="hello")) with open("test.txt") as f: assert f.read() == "hello" mockerfixture를 통해 mock 관련 클래스와 헬퍼에 직접 접근할 수 있습니다.- 자동 정리(Auto-cleanup) — 테스트 후 자동 원복
mocker로 패치한 모든 것은 테스트 종료 후 자동으로 원상복구됩니다.unittest.mock.patch를 데코레이터나 컨텍스트 매니저로 사용할 때와 달리, 명시적으로stop()을 호출하거나with블록을 감쌀 필요가 없습니다.
3. 설치 방법
# pytest 설치 pip install pytest # pytest-mock 설치 pip install pytest-mock # 또는 한 번에 pip install pytest pytest-mockpyproject.toml에 개발 의존성으로 추가하는 것을 권장합니다.[project.optional-dependencies] dev = [ "pytest>=8.0", "pytest-mock>=3.14", ]
4. 사용 예시
4-1. 기본 단위 테스트
# src/calculator.py def add(a: int, b: int) -> int: return a + b def divide(a: float, b: float) -> float: if b == 0: raise ValueError("분모는 0이 될 수 없습니다.") return a / b# tests/test_calculator.py import pytest from src.calculator import add, divide class TestAdd: def test_positive_numbers(self): assert add(1, 2) == 3 def test_negative_numbers(self): assert add(-1, -2) == -3 @pytest.mark.parametrize("a, b, expected", [ (0, 0, 0), (100, -100, 0), (1_000_000, 1, 1_000_001), ]) def test_parametrized(self, a, b, expected): assert add(a, b) == expected class TestDivide: def test_normal_division(self): assert divide(10, 2) == 5.0 def test_divide_by_zero(self): with pytest.raises(ValueError, match="분모는 0이 될 수 없습니다"): divide(10, 0)4-2. Fixture를 활용한 테스트
# tests/conftest.py import pytest from myapp.models import User from myapp.database import SessionLocal @pytest.fixture def db_session(): session = SessionLocal() yield session session.rollback() session.close() @pytest.fixture def sample_user(db_session): user = User(name="테스트유저", email="test@example.com") db_session.add(user) db_session.commit() return user# tests/test_user.py def test_user_exists(sample_user, db_session): found = db_session.query(User).filter_by(email="test@example.com").first() assert found is not None assert found.name == "테스트유저"4-3. pytest-mock 활용 예시
# src/notification.py import smtplib class NotificationService: def send_email(self, to: str, subject: str, body: str) -> bool: with smtplib.SMTP("smtp.example.com") as server: server.sendmail("noreply@example.com", to, body) return True def notify_user(self, user_email: str, message: str) -> bool: return self.send_email( to=user_email, subject="알림", body=message )# tests/test_notification.py from src.notification import NotificationService def test_notify_user_calls_send_email(mocker): service = NotificationService() # send_email을 mock으로 대체 mock_send = mocker.patch.object(service, "send_email", return_value=True) result = service.notify_user("user@example.com", "안녕하세요") assert result is True mock_send.assert_called_once_with( to="user@example.com", subject="알림", body="안녕하세요" ) def test_notify_user_handles_failure(mocker): service = NotificationService() mocker.patch.object(service, "send_email", side_effect=Exception("SMTP 오류")) import pytest with pytest.raises(Exception, match="SMTP 오류"): service.notify_user("user@example.com", "테스트")
5. 언제 사용하고 왜 좋은가
5-1. pytest를 사용해야 하는 경우
5-2. pytest-mock을 사용해야 하는 경우
6. 두 라이브러리의 조합 활용
pytest와 pytest-mock은 함께 사용할 때 시너지가 극대화됩니다. 아래는 실전에서 자주 활용되는 조합 패턴입니다.
6-1. Fixture + mocker: 공통 Mock 객체 재사용
# tests/conftest.py import pytest @pytest.fixture def mock_email_service(mocker): """재사용 가능한 mock email service fixture""" mock = mocker.patch("myapp.services.email.send_email") mock.return_value = True return mock @pytest.fixture def mock_payment_api(mocker): mock = mocker.patch("myapp.external.payment.charge") mock.return_value = {"status": "success", "transaction_id": "TX_001"} return mock# tests/test_order.py def test_order_sends_confirmation_email(mock_email_service, mock_payment_api): order = create_order(user_id=1, amount=50000) order.complete() mock_payment_api.assert_called_once() mock_email_service.assert_called_once_with( to="user@example.com", subject="주문 완료", )6-2. Parametrize + mocker: 여러 응답 케이스 테스트
import pytest @pytest.mark.parametrize("api_response, expected_status", [ ({"status": "success"}, "completed"), ({"status": "pending"}, "processing"), ({"status": "failed"}, "failed"), ]) def test_payment_status_mapping(mocker, api_response, expected_status): mocker.patch("myapp.external.payment.get_status", return_value=api_response) result = PaymentService().check_status("TX_001") assert result == expected_status6-3. Fixture scope + mocker: 모듈 범위 Mock 설정
@pytest.fixture(scope="module") def mock_feature_flag(mocker): """모듈 전체에서 feature flag를 활성화""" with mocker.patch("myapp.config.is_feature_enabled", return_value=True): yield6-4. spy + assert: 내부 동작 검증
def test_cache_is_used_on_second_call(mocker): service = UserService() spy_db = mocker.spy(service, "_fetch_from_db") # 첫 번째 호출: DB에서 가져옴 result1 = service.get_user(1) assert spy_db.call_count == 1 # 두 번째 호출: 캐시에서 가져옴 (DB 미호출) result2 = service.get_user(1) assert spy_db.call_count == 1 # 여전히 1번만 호출됨 assert result1 == result26-5. 비동기(Async) 테스트
# pip install pytest-asyncio import pytest @pytest.mark.asyncio async def test_async_api_call(mocker): mock_fetch = mocker.patch( "myapp.api.fetch_user", new_callable=mocker.AsyncMock, return_value={"id": 1, "name": "홍길동"} ) result = await UserService().get_user_async(1) assert result["name"] == "홍길동" mock_fetch.assert_awaited_once_with(1)
7. 모범 사례 및 팁
- 테스트 파일 및 함수 네이밍 규칙
- 파일:
test_user_service.py - 클래스:
TestUserService - 함수:
test_create_user_with_valid_datatest_create_user_returns_id_when_email_is_valid - 테스트 이름은 what/when/then 구조로 작성하는 것을 권장합니다.
- 파일:
- pytest는 기본적으로
test_*.py또는*_test.py파일을 자동으로 발견합니다. - pytest.ini / pyproject.toml 설정
# pyproject.toml [tool.pytest.ini_options] testpaths = ["tests"] addopts = "-v --tb=short" markers = [ "slow: 느린 테스트 (외부 API, DB 포함)", "integration: 통합 테스트", "unit: 단위 테스트", ]- mocker vs monkeypatch 선택 기준
| 상황 | 권장 도구 | 이유 | | --- | --- | --- | | 함수/메서드 대체 및 호출 검증 | mocker.patch | assert_called_with 등 검증 API 풍부 | | 환경 변수 임시 변경 | monkeypatch.setenv | pytest 내장, 단순 값 변경에 적합 | | 딕셔너리/리스트 임시 수정 | monkeypatch.setitem | pytest 내장, 값 변경에 최적 | | 실제 동작 유지하며 호출 추적 | mocker.spy | spy_return, spy_exception 제공 |- 과도한 Mock 사용 주의사항
- ⚠️Mock이 많을수록 테스트의 신뢰도가 낮아질 수 있습니다.
- 모든 의존성을 Mock으로 대체하면 단위 테스트는 통과하더라도 실제 통합 환경에서 오류가 발생할 수 있습니다. 핵심 비즈니스 로직은 실제 의존성으로 테스트하는 통합 테스트도 병행하는 것을 권장합니다.
8. 결론
pytest와 pytest-mock은 현대 Python 개발에서 사실상 표준(de facto standard) 테스트 스택입니다.
- pytest는 간결한 문법, 강력한 Fixture 시스템, 자동 테스트 발견으로 테스트 작성의 진입 장벽을 크게 낮춰줍니다.
- pytest-mock은 외부 의존성 격리를 위한 Mock 패턴을 pytest의 방식으로 자연스럽게 통합해,
unittest.mock을 직접 사용할 때의 boilerplate를 제거합니다.
두 라이브러리를 함께 활용하면 빠르고 신뢰할 수 있으며 유지보수하기 쉬운 테스트 코드를 작성할 수 있습니다. 외부 의존성은 mock으로 격리하고, fixture로 설정을 공유하며, parametrize로 엣지 케이스를 망라하는 것이 핵심 원칙입니다.
- Fixture — 테스트 의존성 및 환경 설정 관리