금융 데이터 분석 파이프라인을 위한 Pytest 단위 테스트 및 Mock 활용 전략

테스트 프레임워크 설정 및 프로젝트 구조

금융 시계열 데이터를 처리하는 분석 파이프라인에서는 수치 계산의 정확성과 외부 API 호출의 안정성이 필수적입니다. 이를 위해 pytest와 커버리지 측정 플러그인을 설치하여 테스트 환경을 구성합니다.

pip install pytest pytest-cov pytest-asyncio freezegun

효율적인 테스트 관리를 위해 소스 코드와 테스트 코드를 명확히 분리하고, 도메인별로 모듈을 나누는 것이 좋습니다.

quant_analysis_pipeline/
├── core/
│   ├── market_data/      # 시세 데이터 수집 모듈
│   ├── indicators/       # 기술적 지표 연산 모듈
│   └── alerts/           # 알림 전송 모듈
├── tests/
│   ├── conftest.py       # 전역 Fixture 및 설정
│   ├── test_market_data.py
│   ├── test_indicators.py
│   └── test_alerts.py
└── pyproject.toml

외부 의존성 격리를 위한 Mock 객체 활용

실제 네트워크 요청 없이 데이터 수집 로직을 검증하기 위해 unittest.mock을 사용하여 외부 API 응답을 모방합니다.

# core/market_data/equity_client.py
def fetch_equity_history(ticker: str, interval: str = "1d") -> dict:
    """외부 벤더를 통해 주식 히스토리 데이터를 조회합니다."""
    # 실제 API 호출 로직
    pass

테스트 코드에서는 네트워크 계층을 패치(Patch)하여 고정된 응답을 반환하도록 설정합니다.

# tests/test_market_data.py
import pytest
from unittest.mock import patch, MagicMock
from core.market_data.equity_client import fetch_equity_history

def test_fetch_equity_history_returns_valid_payload():
    """정상적인 API 응답 시 데이터 파싱 로직을 검증합니다."""
    mock_api_response = MagicMock()
    mock_api_response.json.return_value = {
        "symbol": "MSFT",
        "prices": [{"date": "2023-10-01", "close": 310.5}]
    }
    mock_api_response.status_code = 200

    with patch('core.market_data.equity_client.requests.get', return_value=mock_api_response):
        payload = fetch_equity_history("MSFT")
        
        assert payload is not None
        assert payload["symbol"] == "MSFT"
        assert len(payload["prices"]) == 1

수치 연산 로직 및 엣지 케이스 검증

기술적 지표 계산은 금융 시스템의 핵심입니다. 이동평균(SMA)이나 상대강도지수(RSI)와 같은 지표가 경계값이나 극단적인 데이터에서 어떻게 동작하는지 확인해야 합니다.

# tests/test_indicators.py
from core.indicators.technical_metrics import compute_sma, compute_rsi

def test_compute_sma_with_custom_window():
    """지정된 기간의 단순이동평균(SMA) 계산 정확도를 테스트합니다."""
    price_series = [10.0, 20.0, 30.0, 40.0, 50.0, 60.0]
    
    sma_values = compute_sma(price_series, window_size=4)
    
    # 4일 SMA는 (10+20+30+40)/4=25, (20+30+40+50)/4=35, (30+40+50+60)/4=45
    assert len(sma_values) == 3
    assert sma_values[-1] == 45.0

def test_compute_rsi_overbought_oversold_conditions():
    """RSI 지표의 과매수 및 과매도 구간 판별 로직을 검증합니다."""
    # 지속적 상승 시나리오
    bullish_trend = [100 + i * 2 for i in range(15)]
    assert compute_rsi(bullish_trend, period=14) > 70.0

    # 지속적 하락 시나리오
    bearish_trend = [100 - i * 2 for i in range(15)]
    assert compute_rsi(bearish_trend, period=14) < 30.0

알림 시스템 테스트 및 예외 처리

슬랙(Slack)이나 이메일과 같은 외부 알림 서비스 연동 시, 성공 및 실패 상황에 대한 예외 처리 로직을 모의 객체로 테스트합니다.

# tests/test_alerts.py
from unittest.mock import patch, MagicMock
from core.alerts.slack_bot import dispatch_slack_notification

def test_dispatch_notification_handles_server_error():
    """슬랙 API 서버 오류(5xx) 발생 시 재시도 또는 실패 플래그 반환을 확인합니다."""
    error_response = MagicMock()
    error_response.status_code = 503
    error_response.raise_for_status.side_effect = Exception("Service Unavailable")

    with patch('core.alerts.slack_bot.httpx.post', return_value=error_response):
        is_sent = dispatch_slack_notification(channel="#quant-signals", text="Anomaly detected")
        
        assert is_sent is False

고급 테스트 기법: 파라미터화 및 Fixture

다양한 입력값에 대한 검증을 단순화하기 위해 @pytest.mark.parametrize를 사용하며, 반복되는 테스트 데이터 준비는 Fixture로 추상화합니다.

# tests/test_indicators.py (계속)
import pytest
import pandas as pd
from core.indicators.volatility import check_price_anomaly

@pytest.fixture
def mock_ohlcv_dataframe():
    """테스트용 OHLCV 데이터프레임을 생성합니다."""
    return pd.DataFrame({
        'open': [100, 102, 101, 105, 120],
        'high': [103, 104, 102, 108, 125],
        'low': [99, 101, 100, 104, 118],
        'close': [102, 101, 101, 107, 122],
        'volume': [5000, 4800, 5100, 6000, 15000]
    })

@pytest.mark.parametrize("threshold,expected_anomaly", [
    (0.05, True),   # 5% 변동성 임계값: 마지막 날 급등으로 이상치 탐지
    (0.30, False),  # 30% 변동성 임계값: 정상 범위 내로 간주
])
def test_price_anomaly_detection(mock_ohlcv_dataframe, threshold, expected_anomaly):
    """가격 변동성 임계값에 따른 이상치 탐지 알고리즘을 테스트합니다."""
    result = check_price_anomaly(mock_ohlcv_dataframe, volatility_threshold=threshold)
    assert result == expected_anomaly

시간 의존적 로직 및 비동기 코드 테스트

장 마감 시간 확인이나 비동기 데이터 스트리밍과 같이 시간 및 이벤트 루프에 의존하는 코드는 freezegunpytest-asyncio를 활용해 제어합니다.

# tests/test_market_data.py (계속)
from freezegun import freeze_time
from core.market_data.scheduler import is_market_open

@freeze_time("2023-11-10 14:30:00") # 미국 동부 시간 기준 장중
def test_market_status_during_trading_hours():
    """특정 시간대에 시장 개방 상태를 올바르게 판단하는지 검증합니다."""
    assert is_market_open(exchange="NYSE") is True

import pytest
from core.market_data.async_stream import stream_realtime_quotes

@pytest.mark.asyncio
async def test_async_quote_streaming():
    """비동기 웹소켓 또는 스트리밍 클라이언트의 데이터 수신 로직을 테스트합니다."""
    quotes = await stream_realtime_quotes(symbols=["TSLA", "NVDA"], duration_sec=2)
    assert len(quotes) > 0
    assert "TSLA" in quotes[0].symbol

테스트 커버리지 측정 및 CI/CD 파이프라인 통합

코드베이스의 테스트 품질을 정량적으로 파악하기 위해 커버리지 리포트를 생성하고, GitHub Actions를 통해 자동화합니다.

# 터미널에서 커버리지 리포트 생성
pytest --cov=core --cov-report=term-missing --cov-report=html tests/

아래는 코드 푸시 및 PR 생성 시 자동으로 테스트를 실행하고 커버리지를 보고하는 GitHub Actions 워크플로우 구성 예시입니다.

# .github/workflows/ci_pipeline.yml
name: Quant Pipeline CI

on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main ]

jobs:
  run_unit_tests:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout Repository
        uses: actions/checkout@v4

      - name: Initialize Python Environment
        uses: actions/setup-python@v5
        with:
          python-version: '3.11'
          cache: 'pip'

      - name: Install Project Dependencies
        run: |
          python -m pip install --upgrade pip
          pip install -r requirements-dev.txt

      - name: Execute Pytest with Coverage
        run: |
          pytest --cov=core --cov-report=xml --cov-fail-under=80 tests/

      - name: Publish Coverage Report
        uses: codecov/codecov-action@v3
        with:
          files: ./coverage.xml
          fail_ci_if_error: true

태그: Pytest python unittest mock github-actions

7월 23일 04:01에 게시됨