|
|
"""
|
|
|
星耀数智(AmazingData)集成测试
|
|
|
|
|
|
测试覆盖:
|
|
|
- API端点测试
|
|
|
- 数据一致性检查
|
|
|
- 多数据源对比
|
|
|
- 完整工作流测试
|
|
|
"""
|
|
|
|
|
|
import unittest
|
|
|
import asyncio
|
|
|
import sys
|
|
|
import os
|
|
|
import json
|
|
|
from datetime import datetime, timedelta
|
|
|
from unittest.mock import Mock, patch, AsyncMock
|
|
|
|
|
|
# 添加项目根目录到路径
|
|
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
|
|
|
|
|
|
|
class TestAPIEndpoints(unittest.TestCase):
|
|
|
"""测试API端点"""
|
|
|
|
|
|
def setUp(self):
|
|
|
"""测试前准备"""
|
|
|
self.base_url = 'http://localhost:8080/v1'
|
|
|
self.headers = {'X-Admin-Token': 'demo-api-key-2024'}
|
|
|
|
|
|
@patch('urllib.request.urlopen')
|
|
|
def test_adapters_endpoint(self, mock_urlopen):
|
|
|
"""测试适配器列表端点"""
|
|
|
# Mock响应
|
|
|
mock_response = Mock()
|
|
|
mock_response.read.return_value = json.dumps({
|
|
|
'code': 200,
|
|
|
'message': 'success',
|
|
|
'data': {
|
|
|
'adapters': [
|
|
|
{'name': 'amazingdata', 'status': 'active', 'type': 'stock'}
|
|
|
]
|
|
|
}
|
|
|
}).encode()
|
|
|
mock_urlopen.return_value = mock_response
|
|
|
|
|
|
import urllib.request
|
|
|
req = urllib.request.Request(
|
|
|
f'{self.base_url}/admin/adapters',
|
|
|
headers=self.headers
|
|
|
)
|
|
|
response = urllib.request.urlopen(req, timeout=10)
|
|
|
data = json.loads(response.read().decode())
|
|
|
|
|
|
self.assertEqual(data['code'], 200)
|
|
|
self.assertIn('adapters', data['data'])
|
|
|
|
|
|
# 检查是否有amazingdata适配器
|
|
|
adapter_names = [a['name'] for a in data['data']['adapters']]
|
|
|
self.assertIn('amazingdata', adapter_names)
|
|
|
|
|
|
@patch('urllib.request.urlopen')
|
|
|
def test_source_status_endpoint(self, mock_urlopen):
|
|
|
"""测试数据源状态端点"""
|
|
|
mock_response = Mock()
|
|
|
mock_response.read.return_value = json.dumps({
|
|
|
'code': 200,
|
|
|
'message': 'success',
|
|
|
'data': {
|
|
|
'sources': [
|
|
|
{'id': 'amazingdata', 'name': '星耀数智', 'status': 'healthy'}
|
|
|
]
|
|
|
}
|
|
|
}).encode()
|
|
|
mock_urlopen.return_value = mock_response
|
|
|
|
|
|
import urllib.request
|
|
|
req = urllib.request.Request(
|
|
|
f'{self.base_url}/admin/source/status',
|
|
|
headers={'X-API-Key': 'demo-api-key-2024'}
|
|
|
)
|
|
|
response = urllib.request.urlopen(req, timeout=10)
|
|
|
data = json.loads(response.read().decode())
|
|
|
|
|
|
self.assertEqual(data['code'], 200)
|
|
|
|
|
|
|
|
|
class TestDataConsistency(unittest.TestCase):
|
|
|
"""测试数据一致性"""
|
|
|
|
|
|
def test_date_format_consistency(self):
|
|
|
"""测试日期格式一致性"""
|
|
|
from app.adapters.amazingdata_adapter import AmazingDataAdapter
|
|
|
|
|
|
adapter = AmazingDataAdapter()
|
|
|
|
|
|
# 测试不同格式的日期转换为统一格式
|
|
|
test_cases = [
|
|
|
('2024-01-01', 20240101),
|
|
|
('2024/01/01', 20240101),
|
|
|
('20240101', 20240101),
|
|
|
(20240101, 20240101),
|
|
|
]
|
|
|
|
|
|
for input_date, expected in test_cases:
|
|
|
result = adapter._format_date(input_date)
|
|
|
self.assertEqual(result, expected, f"Failed for {input_date}")
|
|
|
|
|
|
def test_symbol_format_consistency(self):
|
|
|
"""测试代码格式一致性"""
|
|
|
# 股票代码应该包含交易所后缀
|
|
|
valid_stock_codes = [
|
|
|
'000001.SZ',
|
|
|
'600000.SH',
|
|
|
'688001.SH',
|
|
|
'430001.BJ'
|
|
|
]
|
|
|
|
|
|
for code in valid_stock_codes:
|
|
|
# 检查格式: 代码.交易所
|
|
|
parts = code.split('.')
|
|
|
self.assertEqual(len(parts), 2, f"Invalid code format: {code}")
|
|
|
self.assertIn(parts[1], ['SH', 'SZ', 'BJ'], f"Invalid exchange: {parts[1]}")
|
|
|
|
|
|
|
|
|
class TestXYSZSpecificFeatures(unittest.TestCase):
|
|
|
"""测试星耀数智特有功能"""
|
|
|
|
|
|
def setUp(self):
|
|
|
"""测试前准备"""
|
|
|
self.adapter = None
|
|
|
try:
|
|
|
from app.adapters.amazingdata_adapter import AmazingDataAdapter
|
|
|
self.adapter_class = AmazingDataAdapter
|
|
|
except ImportError:
|
|
|
self.skipTest("AmazingData adapter not available")
|
|
|
|
|
|
def test_futures_exchange_mapping(self):
|
|
|
"""测试期货交易所映射"""
|
|
|
adapter = self.adapter_class()
|
|
|
|
|
|
# 测试上海期货交易所品种
|
|
|
self.assertEqual(adapter._get_futures_exchange('CU'), 'SHFE')
|
|
|
self.assertEqual(adapter._get_futures_exchange('AU'), 'SHFE')
|
|
|
|
|
|
# 测试大连商品交易所品种
|
|
|
self.assertEqual(adapter._get_futures_exchange('A'), 'DCE')
|
|
|
self.assertEqual(adapter._get_futures_exchange('M'), 'DCE')
|
|
|
|
|
|
# 测试郑州商品交易所品种
|
|
|
self.assertEqual(adapter._get_futures_exchange('CF'), 'CZCE')
|
|
|
self.assertEqual(adapter._get_futures_exchange('SR'), 'CZCE')
|
|
|
|
|
|
# 测试中金所品种
|
|
|
self.assertEqual(adapter._get_futures_exchange('IF'), 'CFFEX')
|
|
|
self.assertEqual(adapter._get_futures_exchange('IC'), 'CFFEX')
|
|
|
|
|
|
# 测试能源中心品种
|
|
|
self.assertEqual(adapter._get_futures_exchange('SC'), 'INE')
|
|
|
|
|
|
|
|
|
class TestWorkflow(unittest.IsolatedAsyncioTestCase):
|
|
|
"""测试完整工作流"""
|
|
|
|
|
|
async def asyncSetUp(self):
|
|
|
"""异步测试前准备"""
|
|
|
from app.adapters.amazingdata_adapter import AmazingDataAdapter
|
|
|
self.adapter = AmazingDataAdapter()
|
|
|
|
|
|
async def test_complete_workflow_mocked(self):
|
|
|
"""测试完整工作流(Mock版本)"""
|
|
|
# Mock所有依赖
|
|
|
self.adapter._ad = Mock()
|
|
|
self.adapter._base_data = Mock()
|
|
|
self.adapter._market_data = Mock()
|
|
|
self.adapter._info_data = Mock()
|
|
|
self.adapter._calendar = Mock()
|
|
|
self.adapter._is_logged_in = True
|
|
|
self.adapter._connected = True
|
|
|
|
|
|
# 1. 获取股票列表
|
|
|
self.adapter._base_data.get_code_list.return_value = [
|
|
|
'000001.SZ', '600000.SH'
|
|
|
]
|
|
|
|
|
|
import pandas as pd
|
|
|
self.adapter._base_data.get_code_info.return_value = pd.DataFrame({
|
|
|
'symbol': ['平安银行', '浦发银行']
|
|
|
}, index=['000001.SZ', '600000.SH'])
|
|
|
|
|
|
symbols = await self.adapter.fetch_symbols('stock')
|
|
|
self.assertEqual(len(symbols), 2)
|
|
|
|
|
|
# 2. 获取K线数据
|
|
|
kline_df = pd.DataFrame({
|
|
|
'open': [10.0], 'high': [11.0], 'low': [9.0],
|
|
|
'close': [10.5], 'volume': [10000], 'amount': [105000]
|
|
|
}, index=pd.to_datetime(['2024-01-01']))
|
|
|
|
|
|
self.adapter._market_data.query_kline.return_value = {
|
|
|
'000001.SZ': kline_df
|
|
|
}
|
|
|
|
|
|
klines = await self.adapter.fetch_klines(
|
|
|
symbol='000001.SZ',
|
|
|
start='20240101',
|
|
|
end='20240101',
|
|
|
freq='1d'
|
|
|
)
|
|
|
self.assertEqual(len(klines), 1)
|
|
|
|
|
|
# 3. 健康检查
|
|
|
self.adapter._base_data.get_code_list.return_value = ['000001.SZ']
|
|
|
health = await self.adapter.health_check()
|
|
|
self.assertTrue(health)
|
|
|
|
|
|
|
|
|
class TestErrorHandling(unittest.TestCase):
|
|
|
"""测试错误处理"""
|
|
|
|
|
|
def test_connection_error(self):
|
|
|
"""测试连接错误处理"""
|
|
|
from app.adapters.amazingdata_adapter import AmazingDataAdapter
|
|
|
|
|
|
adapter = AmazingDataAdapter()
|
|
|
|
|
|
# 测试未登录时调用方法
|
|
|
with self.assertRaises(RuntimeError) as context:
|
|
|
adapter._check_login()
|
|
|
|
|
|
self.assertIn('未连接到数据源', str(context.exception))
|
|
|
|
|
|
def test_invalid_date_format(self):
|
|
|
"""测试无效日期格式处理"""
|
|
|
from app.adapters.amazingdata_adapter import AmazingDataAdapter
|
|
|
|
|
|
adapter = AmazingDataAdapter()
|
|
|
|
|
|
# 测试无效输入
|
|
|
with self.assertRaises(ValueError):
|
|
|
adapter._format_date(None)
|
|
|
|
|
|
with self.assertRaises(ValueError):
|
|
|
adapter._format_date([])
|
|
|
|
|
|
with self.assertRaises(ValueError):
|
|
|
adapter._format_date({'date': '2024-01-01'})
|
|
|
|
|
|
|
|
|
class TestPerformanceRequirements(unittest.TestCase):
|
|
|
"""测试性能要求"""
|
|
|
|
|
|
def test_large_symbol_list_handling(self):
|
|
|
"""测试大批量代码处理"""
|
|
|
from app.adapters.amazingdata_adapter import AmazingDataAdapter
|
|
|
|
|
|
adapter = AmazingDataAdapter()
|
|
|
|
|
|
# 模拟5000只股票
|
|
|
large_code_list = [f'{i:06d}.SZ' for i in range(1, 5001)]
|
|
|
|
|
|
# 分批处理测试
|
|
|
batch_size = 50
|
|
|
batches = [large_code_list[i:i+batch_size] for i in range(0, len(large_code_list), batch_size)]
|
|
|
|
|
|
self.assertEqual(len(batches), 100)
|
|
|
self.assertEqual(len(batches[0]), 50)
|
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
unittest.main(verbosity=2)
|