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:
Ahmet Guzererler 2026-03-19 13:51:51 +01:00
parent d2af8991ed
commit 8c9183cf10
27 changed files with 121 additions and 1 deletions

View File

@ -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
tests/cassettes/.gitkeep Normal file
View File

0
tests/e2e/__init__.py Normal file
View File

10
tests/e2e/conftest.py Normal file
View File

@ -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)

View File

View File

@ -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
tests/unit/__init__.py Normal file
View File

73
tests/unit/conftest.py Normal file
View File

@ -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