Add integration and unit tests for scanner routing, TTM analysis, vendor fail-fast, and yfinance data layer
- Implement integration tests for scanner vendor routing, ensuring correct routing to Alpha Vantage and fallback to yfinance. - Create comprehensive unit tests for TTM analysis, covering metrics computation and report formatting. - Introduce fail-fast vendor routing tests to verify immediate failure for methods not in FALLBACK_ALLOWED. - Develop extensive integration tests for the yfinance data layer, mocking external calls to validate functionality across various financial data retrieval methods.
This commit is contained in:
parent
d2af8991ed
commit
8c9183cf10
|
|
@ -43,10 +43,18 @@ include = ["tradingagents*", "cli*"]
|
|||
[dependency-groups]
|
||||
dev = [
|
||||
"pytest>=9.0.2",
|
||||
"pytest-recording>=0.13.2",
|
||||
"vcrpy>=6.0.2",
|
||||
"pytest-socket>=0.7.0",
|
||||
]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = ["tests"]
|
||||
markers = [
|
||||
"integration: marks tests as live integration tests requiring real API keys",
|
||||
"integration: tests that replay VCR cassettes of data API calls",
|
||||
"e2e: tests that hit real LLM APIs (manual trigger only)",
|
||||
"vcr: tests that use VCR cassette recording",
|
||||
"slow: tests that take a long time to run",
|
||||
"paid_tier: marks tests that require a paid Finnhub subscription (free tier returns HTTP 403)",
|
||||
]
|
||||
addopts = "--ignore=tests/integration --ignore=tests/e2e --disable-socket --allow-unix-socket -x -q"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
"""E2E test configuration — real LLM API calls, manual trigger only."""
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
def pytest_collection_modifyitems(config, items):
|
||||
"""Mark all e2e tests as slow."""
|
||||
for item in items:
|
||||
item.add_marker(pytest.mark.e2e)
|
||||
item.add_marker(pytest.mark.slow)
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
"""Integration test configuration — VCR cassette replay for data API tests."""
|
||||
|
||||
import os
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def vcr_config():
|
||||
return {
|
||||
"cassette_library_dir": "tests/cassettes",
|
||||
"record_mode": "none",
|
||||
"match_on": ["method", "scheme", "host", "port", "path"],
|
||||
"filter_headers": [
|
||||
"Authorization",
|
||||
"Cookie",
|
||||
"X-Api-Key",
|
||||
],
|
||||
"filter_query_parameters": [
|
||||
"apikey",
|
||||
"token",
|
||||
],
|
||||
"decode_compressed_response": True,
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def av_api_key():
|
||||
"""Return the Alpha Vantage API key for integration tests."""
|
||||
return os.environ.get("ALPHA_VANTAGE_API_KEY", "demo")
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
"""Shared mock factories for unit tests.
|
||||
|
||||
Network is blocked by pytest-socket (--disable-socket in addopts).
|
||||
No test in tests/unit/ can hit a real API.
|
||||
"""
|
||||
|
||||
import json
|
||||
import pandas as pd
|
||||
import pytest
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
|
||||
# -- yfinance mock factories --
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_yf_screener():
|
||||
"""Pre-built yfinance screener mock."""
|
||||
|
||||
def _make(quotes):
|
||||
return {"quotes": quotes}
|
||||
|
||||
return _make
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_yf_download():
|
||||
"""Pre-built yfinance download mock returning a MultiIndex DataFrame."""
|
||||
|
||||
def _make(symbols, periods=5, base_price=100.0):
|
||||
idx = pd.date_range("2024-01-04", periods=periods, freq="B")
|
||||
data = {s: [base_price + i for i in range(periods)] for s in symbols}
|
||||
df = pd.DataFrame(data, index=idx)
|
||||
df.columns = pd.MultiIndex.from_product([["Close"], symbols])
|
||||
return df
|
||||
|
||||
return _make
|
||||
|
||||
|
||||
# -- Alpha Vantage mock factories --
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_av_request():
|
||||
"""Pre-built Alpha Vantage _rate_limited_request mock."""
|
||||
|
||||
def _make(responses: dict):
|
||||
"""responses: {function_name: return_value} or callable."""
|
||||
|
||||
def fake(function_name, params=None, **kwargs):
|
||||
if callable(responses.get(function_name)):
|
||||
return responses[function_name](params)
|
||||
return json.dumps(responses.get(function_name, {}))
|
||||
|
||||
return fake
|
||||
|
||||
return _make
|
||||
|
||||
|
||||
# -- LLM mock factories --
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_llm():
|
||||
"""Pre-built LLM mock that returns canned responses."""
|
||||
|
||||
def _make(content="Mocked LLM response."):
|
||||
llm = MagicMock()
|
||||
llm.invoke.return_value = MagicMock(content=content)
|
||||
llm.ainvoke.return_value = MagicMock(content=content)
|
||||
return llm
|
||||
|
||||
return _make
|
||||
Loading…
Reference in New Issue