TCPDashboard/tests/test_chart_builder.py
Vasily.onl c4ec3fac9f 3.4 Enhance logging and modular chart system for Crypto Trading Bot Dashboard
- Suppressed SQLAlchemy logging in `app.py` and `main.py` to reduce console verbosity.
- Introduced a new modular chart system in `components/charts/` with a `ChartBuilder` class for flexible chart creation.
- Added utility functions for data processing and validation in `components/charts/utils.py`.
- Implemented indicator definitions and configurations in `components/charts/config/indicator_defs.py`.
- Created a comprehensive documentation structure for the new chart system, ensuring clarity and maintainability.
- Added unit tests for the `ChartBuilder` class to verify functionality and robustness.
- Updated existing components to integrate with the new chart system, enhancing overall architecture and user experience.
2025-06-03 12:49:46 +08:00

306 lines
11 KiB
Python

#!/usr/bin/env python3
"""
Unit Tests for ChartBuilder Class
Tests for the core ChartBuilder functionality including:
- Chart creation
- Data fetching
- Error handling
- Market data integration
"""
import pytest
import pandas as pd
from datetime import datetime, timezone, timedelta
from unittest.mock import Mock, patch, MagicMock
from typing import List, Dict, Any
import sys
from pathlib import Path
project_root = Path(__file__).parent.parent
sys.path.insert(0, str(project_root))
from components.charts.builder import ChartBuilder
from components.charts.utils import validate_market_data, prepare_chart_data
class TestChartBuilder:
"""Test suite for ChartBuilder class"""
@pytest.fixture
def mock_logger(self):
"""Mock logger for testing"""
return Mock()
@pytest.fixture
def chart_builder(self, mock_logger):
"""Create ChartBuilder instance for testing"""
return ChartBuilder(mock_logger)
@pytest.fixture
def sample_candles(self):
"""Sample candle data for testing"""
base_time = datetime.now(timezone.utc) - timedelta(hours=24)
return [
{
'timestamp': base_time + timedelta(minutes=i),
'open': 50000 + i * 10,
'high': 50100 + i * 10,
'low': 49900 + i * 10,
'close': 50050 + i * 10,
'volume': 1000 + i * 5,
'exchange': 'okx',
'symbol': 'BTC-USDT',
'timeframe': '1m'
}
for i in range(100)
]
def test_chart_builder_initialization(self, mock_logger):
"""Test ChartBuilder initialization"""
builder = ChartBuilder(mock_logger)
assert builder.logger == mock_logger
assert builder.db_ops is not None
assert builder.default_colors is not None
assert builder.default_height == 600
assert builder.default_template == "plotly_white"
def test_chart_builder_default_logger(self):
"""Test ChartBuilder initialization with default logger"""
builder = ChartBuilder()
assert builder.logger is not None
@patch('components.charts.builder.get_database_operations')
def test_fetch_market_data_success(self, mock_db_ops, chart_builder, sample_candles):
"""Test successful market data fetching"""
# Mock database operations
mock_db = Mock()
mock_db.market_data.get_candles.return_value = sample_candles
mock_db_ops.return_value = mock_db
# Replace the db_ops attribute with our mock
chart_builder.db_ops = mock_db
# Test fetch
result = chart_builder.fetch_market_data('BTC-USDT', '1m', days_back=1)
assert result == sample_candles
mock_db.market_data.get_candles.assert_called_once()
@patch('components.charts.builder.get_database_operations')
def test_fetch_market_data_empty(self, mock_db_ops, chart_builder):
"""Test market data fetching with empty result"""
# Mock empty database result
mock_db = Mock()
mock_db.market_data.get_candles.return_value = []
mock_db_ops.return_value = mock_db
# Replace the db_ops attribute with our mock
chart_builder.db_ops = mock_db
result = chart_builder.fetch_market_data('BTC-USDT', '1m')
assert result == []
@patch('components.charts.builder.get_database_operations')
def test_fetch_market_data_exception(self, mock_db_ops, chart_builder):
"""Test market data fetching with database exception"""
# Mock database exception
mock_db = Mock()
mock_db.market_data.get_candles.side_effect = Exception("Database error")
mock_db_ops.return_value = mock_db
# Replace the db_ops attribute with our mock
chart_builder.db_ops = mock_db
result = chart_builder.fetch_market_data('BTC-USDT', '1m')
assert result == []
chart_builder.logger.error.assert_called()
def test_create_candlestick_chart_with_data(self, chart_builder, sample_candles):
"""Test candlestick chart creation with valid data"""
# Mock fetch_market_data to return sample data
chart_builder.fetch_market_data = Mock(return_value=sample_candles)
fig = chart_builder.create_candlestick_chart('BTC-USDT', '1m')
assert fig is not None
assert len(fig.data) >= 1 # Should have at least candlestick trace
assert 'BTC-USDT' in fig.layout.title.text
def test_create_candlestick_chart_with_volume(self, chart_builder, sample_candles):
"""Test candlestick chart creation with volume subplot"""
chart_builder.fetch_market_data = Mock(return_value=sample_candles)
fig = chart_builder.create_candlestick_chart('BTC-USDT', '1m', include_volume=True)
assert fig is not None
assert len(fig.data) >= 2 # Should have candlestick + volume traces
def test_create_candlestick_chart_no_data(self, chart_builder):
"""Test candlestick chart creation with no data"""
chart_builder.fetch_market_data = Mock(return_value=[])
fig = chart_builder.create_candlestick_chart('BTC-USDT', '1m')
assert fig is not None
# Check for annotation with message instead of title
assert len(fig.layout.annotations) > 0
assert "No data available" in fig.layout.annotations[0].text
def test_create_candlestick_chart_invalid_data(self, chart_builder):
"""Test candlestick chart creation with invalid data"""
invalid_data = [{'invalid': 'data'}]
chart_builder.fetch_market_data = Mock(return_value=invalid_data)
fig = chart_builder.create_candlestick_chart('BTC-USDT', '1m')
assert fig is not None
# Should show error chart
assert len(fig.layout.annotations) > 0
assert "Invalid market data" in fig.layout.annotations[0].text
def test_create_strategy_chart_basic_implementation(self, chart_builder, sample_candles):
"""Test strategy chart creation (currently returns basic chart)"""
chart_builder.fetch_market_data = Mock(return_value=sample_candles)
result = chart_builder.create_strategy_chart('BTC-USDT', '1m', 'test_strategy')
assert result is not None
# Should currently return a basic candlestick chart
assert 'BTC-USDT' in result.layout.title.text
def test_create_empty_chart(self, chart_builder):
"""Test empty chart creation"""
fig = chart_builder._create_empty_chart("Test message")
assert fig is not None
assert len(fig.layout.annotations) > 0
assert "Test message" in fig.layout.annotations[0].text
assert len(fig.data) == 0
def test_create_error_chart(self, chart_builder):
"""Test error chart creation"""
fig = chart_builder._create_error_chart("Test error")
assert fig is not None
assert len(fig.layout.annotations) > 0
assert "Test error" in fig.layout.annotations[0].text
class TestChartBuilderIntegration:
"""Integration tests for ChartBuilder with real components"""
@pytest.fixture
def chart_builder(self):
"""Create ChartBuilder for integration testing"""
return ChartBuilder()
def test_market_data_validation_integration(self, chart_builder):
"""Test integration with market data validation"""
# Test with valid data structure
valid_data = [
{
'timestamp': datetime.now(timezone.utc),
'open': 50000,
'high': 50100,
'low': 49900,
'close': 50050,
'volume': 1000
}
]
assert validate_market_data(valid_data) is True
def test_chart_data_preparation_integration(self, chart_builder):
"""Test integration with chart data preparation"""
raw_data = [
{
'timestamp': datetime.now(timezone.utc) - timedelta(hours=1),
'open': '50000', # String values to test conversion
'high': '50100',
'low': '49900',
'close': '50050',
'volume': '1000'
},
{
'timestamp': datetime.now(timezone.utc),
'open': '50050',
'high': '50150',
'low': '49950',
'close': '50100',
'volume': '1200'
}
]
df = prepare_chart_data(raw_data)
assert isinstance(df, pd.DataFrame)
assert len(df) == 2
assert all(col in df.columns for col in ['timestamp', 'open', 'high', 'low', 'close', 'volume'])
assert df['open'].dtype.kind in 'fi' # Float or integer
class TestChartBuilderEdgeCases:
"""Test edge cases and error conditions"""
@pytest.fixture
def chart_builder(self):
return ChartBuilder()
def test_chart_creation_with_single_candle(self, chart_builder):
"""Test chart creation with only one candle"""
single_candle = [{
'timestamp': datetime.now(timezone.utc),
'open': 50000,
'high': 50100,
'low': 49900,
'close': 50050,
'volume': 1000
}]
chart_builder.fetch_market_data = Mock(return_value=single_candle)
fig = chart_builder.create_candlestick_chart('BTC-USDT', '1m')
assert fig is not None
assert len(fig.data) >= 1
def test_chart_creation_with_missing_volume(self, chart_builder):
"""Test chart creation with missing volume data"""
no_volume_data = [{
'timestamp': datetime.now(timezone.utc),
'open': 50000,
'high': 50100,
'low': 49900,
'close': 50050
# No volume field
}]
chart_builder.fetch_market_data = Mock(return_value=no_volume_data)
fig = chart_builder.create_candlestick_chart('BTC-USDT', '1m', include_volume=True)
assert fig is not None
# Should handle missing volume gracefully
def test_chart_creation_with_none_values(self, chart_builder):
"""Test chart creation with None values in data"""
data_with_nulls = [{
'timestamp': datetime.now(timezone.utc),
'open': 50000,
'high': None, # Null value
'low': 49900,
'close': 50050,
'volume': 1000
}]
chart_builder.fetch_market_data = Mock(return_value=data_with_nulls)
fig = chart_builder.create_candlestick_chart('BTC-USDT', '1m')
assert fig is not None
# Should handle null values gracefully
if __name__ == '__main__':
# Run tests if executed directly
pytest.main([__file__, '-v'])