You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
buffer_platform/buffer_platform_v2/tests/conftest.py

286 lines
8.0 KiB

"""测试配置与公共Fixtures"""
from collections.abc import AsyncGenerator, Generator
from contextlib import asynccontextmanager
from datetime import datetime, timedelta
from typing import Any
from unittest.mock import AsyncMock, MagicMock
import pytest
from fastapi.testclient import TestClient
from sqlalchemy import create_engine
from sqlalchemy.orm import Session, sessionmaker
# ---------------------------------------------------------------------------
# Mock 结果辅助类
# ---------------------------------------------------------------------------
class MockScalarResult:
"""模拟 SQLAlchemy ScalarResult"""
def __init__(self, items: list[Any] | None = None):
self._items = items or []
def all(self) -> list[Any]:
return list(self._items)
def first(self) -> Any:
return self._items[0] if self._items else None
def one_or_none(self) -> Any:
if len(self._items) == 1:
return self._items[0]
if len(self._items) == 0:
return None
raise Exception("Multiple rows found")
def __iter__(self):
return iter(self._items)
class MockResult:
"""模拟 SQLAlchemy Result"""
def __init__(self, items: list[Any] | None = None, scalar: Any = None):
self._items = items or []
self._scalar = scalar
def scalars(self) -> MockScalarResult:
return MockScalarResult(self._items)
def scalar_one_or_none(self) -> Any:
if self._scalar is not None:
return self._scalar
if len(self._items) == 1:
return self._items[0]
if len(self._items) == 0:
return None
return self._items[0]
def scalar(self) -> Any:
return self._scalar
def first(self) -> Any:
return self._items[0] if self._items else None
def all(self) -> list[Any]:
return list(self._items)
def make_mock_db(return_items: list[Any] | None = None, return_scalar: Any = None) -> AsyncMock:
"""创建一个模拟的异步数据库会话。
Args:
return_items: execute() 返回的列表数据
return_scalar: execute() 返回的标量数据
"""
mock_db = AsyncMock()
result = MockResult(items=return_items, scalar=return_scalar)
mock_db.execute = AsyncMock(return_value=result)
mock_db.flush = AsyncMock()
mock_db.commit = AsyncMock()
mock_db.rollback = AsyncMock()
mock_db.refresh = AsyncMock()
mock_db.delete = MagicMock()
mock_db.add = MagicMock()
return mock_db
# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------
@pytest.fixture
def db_session() -> Generator[Session, None, None]:
"""创建测试用数据库会话内存SQLite"""
engine = create_engine("sqlite:///:memory:")
from app.models.analysis import Base as AnalysisBase
from app.models.market import Base as MarketBase
from app.models.user import Base as UserBase
MarketBase.metadata.create_all(engine)
AnalysisBase.metadata.create_all(engine)
UserBase.metadata.create_all(engine)
session_factory = sessionmaker(bind=engine)
session = session_factory()
yield session
session.close()
@asynccontextmanager
async def _mock_lifespan(app: Any) -> AsyncGenerator[None, None]:
"""模拟应用生命周期,跳过真实数据库/调度器初始化。"""
yield
@pytest.fixture
def mock_session() -> AsyncMock:
"""创建一个可配置的 mock 数据库会话"""
return make_mock_db()
@pytest.fixture
def client(mock_session: AsyncMock) -> Generator[TestClient, None, None]:
"""创建FastAPI测试客户端mock lifespan + mock get_db"""
from app.database import get_db
from app.main import app
app.router.lifespan_context = _mock_lifespan
async def _override_get_db() -> AsyncGenerator[Any, None]:
yield mock_session
app.dependency_overrides[get_db] = _override_get_db
with TestClient(app) as c:
yield c
app.dependency_overrides.clear()
@pytest.fixture
def client_factory() -> Any:
"""创建一个可以自定义 mock_session 的客户端工厂"""
from app.database import get_db
from app.main import app
app.router.lifespan_context = _mock_lifespan
def _create_client(return_items=None, return_scalar=None):
mock_db = make_mock_db(return_items=return_items, return_scalar=return_scalar)
async def _override_get_db() -> AsyncGenerator[Any, None]:
yield mock_db
app.dependency_overrides[get_db] = _override_get_db
return TestClient(app), mock_db
yield _create_client
app.dependency_overrides.clear()
@pytest.fixture
def mock_akshare() -> Any:
"""Mock akshare数据采集返回样本K线数据"""
mock = AsyncMock()
mock.fetch_symbol_data.return_value = _build_sample_result()
return mock
@pytest.fixture
def sample_kline_data() -> list[dict[str, Any]]:
"""样本K线数据"""
base_date = datetime(2025, 1, 2)
data: list[dict[str, Any]] = []
for i in range(100):
data.append(
{
"date": (base_date + timedelta(days=i)).strftime("%Y-%m-%d"),
"open": 4000.0 + i * 2,
"high": 4050.0 + i * 2,
"low": 3980.0 + i * 2,
"close": 4020.0 + i * 2,
"volume": 10000 + i * 100,
"hold": 50000 + i * 50,
}
)
return data
@pytest.fixture
def sample_symbols_config() -> list[dict[str, Any]]:
"""样本品种配置"""
return [
{
"symbol": "rb2510",
"variety": "螺纹钢",
"exchange": "SHFE",
"enabled": True,
"periods": ["1d", "4h", "1h"],
},
{
"symbol": "i2510",
"variety": "铁矿石",
"exchange": "DCE",
"enabled": True,
"periods": ["1d", "4h"],
},
{
"symbol": "MA510",
"variety": "甲醇",
"exchange": "CZCE",
"enabled": True,
"periods": ["1d"],
},
]
# ---------------------------------------------------------------------------
# 辅助函数
# ---------------------------------------------------------------------------
def _build_sample_kline_df() -> Any:
"""构建样本K线 DataFrame供 mock_akshare 使用)"""
try:
import pandas as pd
except ImportError:
return None
base_date = datetime(2025, 1, 2)
rows = []
for i in range(100):
rows.append(
{
"date": (base_date + timedelta(days=i)).strftime("%Y%m%d"),
"open": 4000.0 + i * 2,
"high": 4050.0 + i * 2,
"low": 3980.0 + i * 2,
"close": 4020.0 + i * 2,
"volume": 10000 + i * 100,
"hold": 50000 + i * 50,
}
)
return pd.DataFrame(rows)
def _build_sample_result() -> dict[str, Any]:
"""构建样本采集结果。"""
return {
"symbol": "rb2510",
"type": "futures",
"current_price": 4020.0,
"timestamp": "2025-01-15T10:00:00",
"timeframes": {
"daily": [
{
"datetime": "2025-01-10",
"open": 4000.0,
"high": 4050.0,
"low": 3980.0,
"close": 4020.0,
"volume": 10000,
}
],
},
}
def make_mock_user(
user_id: int = 1,
username: str = "admin",
role: str = "admin",
email: str = "admin@test.com",
) -> Any:
"""创建一个模拟用户对象。"""
class MockUser:
def __init__(self) -> None:
self.id = user_id
self.username = username
self.role = role
self.email = email
self.is_active = True
self.created_at = datetime(2025, 1, 1)
self.last_login = datetime(2025, 1, 15)
return MockUser()