#!/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'])