Skip to content

Latest commit

 

History

History
940 lines (755 loc) · 27.9 KB

File metadata and controls

940 lines (755 loc) · 27.9 KB

Testing Guide

This guide covers comprehensive testing strategies for ForexSmartBot, including unit tests, integration tests, performance tests, and security tests.

Table of Contents

Testing Overview

Testing Philosophy

  1. Test-Driven Development: Write tests before code
  2. Comprehensive Coverage: Test all critical paths
  3. Automated Testing: Automate as much as possible
  4. Continuous Testing: Test continuously in CI/CD
  5. Quality Assurance: Ensure high quality standards

Testing Pyramid

    /\
   /  \
  / UI \     (Few, slow, expensive)
 /______\
/        \
/Integration\  (Some, medium speed, cost)
/____________\
/              \
/   Unit Tests   \  (Many, fast, cheap)
/________________\

Test Metrics

  • Code Coverage: 95%+ for critical components
  • Test Execution Time: < 5 minutes for full suite
  • Test Reliability: 99%+ pass rate
  • Test Maintainability: Easy to update and extend

Test Types

Unit Tests

  • Purpose: Test individual functions and methods
  • Scope: Single component or function
  • Speed: Fast (< 1 second per test)
  • Frequency: Run on every commit
  • Tools: pytest, unittest

Integration Tests

  • Purpose: Test component interactions
  • Scope: Multiple components working together
  • Speed: Medium (1-10 seconds per test)
  • Frequency: Run on every pull request
  • Tools: pytest, testcontainers

Performance Tests

  • Purpose: Test system performance under load
  • Scope: Entire system or major components
  • Speed: Slow (10+ seconds per test)
  • Frequency: Run nightly or on demand
  • Tools: pytest-benchmark, locust

Security Tests

  • Purpose: Test security vulnerabilities
  • Scope: Security-sensitive components
  • Speed: Medium to slow
  • Frequency: Run weekly or on demand
  • Tools: bandit, safety, semgrep

UI Tests

  • Purpose: Test user interface functionality
  • Scope: UI components and user workflows
  • Speed: Slow (5+ seconds per test)
  • Frequency: Run on major releases
  • Tools: pytest-qt, selenium

Unit Testing

Test Structure

# tests/test_risk_engine.py
import pytest
from forexsmartbot.core.risk_engine import RiskEngine, RiskConfig

class TestRiskEngine:
    def setup_method(self):
        """Set up test fixtures before each test method."""
        self.config = RiskConfig(
            base_risk_pct=0.02,
            max_trade_amount=1000.0,
            min_trade_amount=10.0
        )
        self.engine = RiskEngine(self.config)
    
    def test_position_sizing_basic(self):
        """Test basic position sizing calculation."""
        # Arrange
        symbol = "EURUSD"
        strategy = "SMA_Crossover"
        balance = 10000.0
        volatility = 0.02
        
        # Act
        size = self.engine.calculate_position_size(symbol, strategy, balance, volatility)
        
        # Assert
        assert size > 0
        assert size <= self.config.max_trade_amount
        assert size >= self.config.min_trade_amount
    
    def test_position_sizing_volatility_targeting(self):
        """Test volatility targeting reduces position size for high volatility."""
        # Arrange
        symbol = "EURUSD"
        strategy = "SMA_Crossover"
        balance = 10000.0
        high_volatility = 0.05
        low_volatility = 0.01
        
        # Act
        size_high_vol = self.engine.calculate_position_size(symbol, strategy, balance, high_volatility)
        size_low_vol = self.engine.calculate_position_size(symbol, strategy, balance, low_volatility)
        
        # Assert
        assert size_high_vol < size_low_vol
    
    def test_daily_risk_limit(self):
        """Test daily risk limit enforcement."""
        # Arrange
        symbol = "EURUSD"
        amount = 500.0
        
        # Act
        result = self.engine.check_daily_risk_limit(symbol, amount)
        
        # Assert
        assert result is True  # Should be within limit
    
    def test_drawdown_limit(self):
        """Test drawdown limit enforcement."""
        # Arrange
        current_equity = 9000.0
        peak_equity = 10000.0
        
        # Act
        result = self.engine.check_drawdown_limit(current_equity, peak_equity)
        
        # Assert
        assert result is True  # 10% drawdown should be within limit
    
    def test_trade_result_tracking(self):
        """Test trade result tracking and analysis."""
        # Arrange
        symbol = "EURUSD"
        strategy = "SMA_Crossover"
        pnl = 100.0
        
        # Act
        self.engine.record_trade_result(symbol, strategy, pnl)
        win_rate = self.engine.get_recent_win_rate(symbol, strategy)
        
        # Assert
        assert win_rate is not None
        assert win_rate == 1.0  # 100% win rate for single winning trade

Test Fixtures

# conftest.py
import pytest
import pandas as pd
from datetime import datetime, timedelta

@pytest.fixture
def sample_data():
    """Provide sample market data for testing."""
    dates = pd.date_range(start='2024-01-01', end='2024-01-31', freq='D')
    data = pd.DataFrame({
        'Date': dates,
        'Open': [1.1000 + i * 0.0001 for i in range(len(dates))],
        'High': [1.1050 + i * 0.0001 for i in range(len(dates))],
        'Low': [1.0950 + i * 0.0001 for i in range(len(dates))],
        'Close': [1.1025 + i * 0.0001 for i in range(len(dates))],
        'Volume': [1000000] * len(dates)
    })
    data.set_index('Date', inplace=True)
    return data

@pytest.fixture
def risk_config():
    """Provide risk configuration for testing."""
    return RiskConfig(
        base_risk_pct=0.02,
        max_risk_pct=0.05,
        min_trade_amount=10.0,
        max_trade_amount=1000.0,
        volatility_target=0.02,
        max_drawdown_pct=0.10
    )

@pytest.fixture
def sample_trades():
    """Provide sample trades for testing."""
    return [
        {'symbol': 'EURUSD', 'side': 1, 'quantity': 100, 'pnl': 50.0},
        {'symbol': 'EURUSD', 'side': -1, 'quantity': 100, 'pnl': -25.0},
        {'symbol': 'GBPUSD', 'side': 1, 'quantity': 100, 'pnl': 75.0},
    ]

Mocking and Stubbing

# tests/test_strategies.py
import pytest
from unittest.mock import Mock, patch
from forexsmartbot.strategies import SMACrossoverStrategy

class TestStrategies:
    def test_sma_crossover_signal(self, sample_data):
        """Test SMA crossover signal generation."""
        # Arrange
        strategy = SMACrossoverStrategy(fast=5, slow=10)
        df = strategy.calculate_indicators(sample_data)
        
        # Act
        signal = strategy.signal(df)
        
        # Assert
        assert signal in [-1, 0, 1]
    
    @patch('forexsmartbot.adapters.data.YFinanceProvider.get_data')
    def test_strategy_with_mocked_data(self, mock_get_data, sample_data):
        """Test strategy with mocked data provider."""
        # Arrange
        mock_get_data.return_value = sample_data
        strategy = SMACrossoverStrategy()
        
        # Act
        df = strategy.calculate_indicators(sample_data)
        signal = strategy.signal(df)
        
        # Assert
        assert signal in [-1, 0, 1]
        mock_get_data.assert_called_once()
    
    def test_strategy_parameters(self):
        """Test strategy parameter handling."""
        # Arrange
        strategy = SMACrossoverStrategy(fast=10, slow=20)
        
        # Act
        parameters = strategy.parameters
        
        # Assert
        assert parameters['fast'] == 10
        assert parameters['slow'] == 20

Integration Testing

Component Integration

# tests/integration/test_trading_controller.py
import pytest
from forexsmartbot.services.controller import TradingController
from forexsmartbot.adapters.brokers import PaperBroker
from forexsmartbot.adapters.data import YFinanceProvider
from forexsmartbot.core.risk_engine import RiskEngine, RiskConfig
from forexsmartbot.core.portfolio import Portfolio

class TestTradingController:
    def setup_method(self):
        """Set up integration test fixtures."""
        self.broker = PaperBroker(initial_balance=10000.0)
        self.data_provider = YFinanceProvider()
        self.risk_engine = RiskEngine(RiskConfig())
        self.portfolio = Portfolio(initial_balance=10000.0)
        
        self.controller = TradingController(
            broker=self.broker,
            data_provider=self.data_provider,
            risk_engine=self.risk_engine,
            portfolio=self.portfolio
        )
    
    def test_full_trading_cycle(self, sample_data):
        """Test complete trading cycle from data to execution."""
        # Arrange
        strategy = SMACrossoverStrategy()
        symbol = "EURUSD"
        
        # Act
        # Start trading
        self.controller.start_trading(strategy, symbol)
        
        # Process data
        df = strategy.calculate_indicators(sample_data)
        signal = strategy.signal(df)
        
        # Execute trade if signal
        if signal != 0:
            self.controller.process_signals()
        
        # Stop trading
        self.controller.stop_trading(symbol)
        
        # Assert
        positions = self.portfolio.get_all_positions()
        trades = self.portfolio.get_trades()
        
        # Verify no positions remain
        assert len(positions) == 0
        
        # Verify trades were recorded
        assert len(trades) >= 0

Database Integration

# tests/integration/test_persistence.py
import pytest
import sqlite3
from forexsmartbot.services.persistence import PersistenceService
from forexsmartbot.core.interfaces import Trade

class TestPersistenceIntegration:
    def setup_method(self):
        """Set up database test fixtures."""
        self.persistence = PersistenceService(data_dir=":memory:")
        self.sample_trade = Trade(
            symbol="EURUSD",
            side=1,
            quantity=100.0,
            entry_price=1.1000,
            exit_price=1.1050,
            pnl=50.0,
            strategy="SMA_Crossover",
            entry_time=datetime.now(),
            exit_time=datetime.now()
        )
    
    def test_trade_persistence(self):
        """Test trade saving and retrieval."""
        # Arrange
        trade = self.sample_trade
        
        # Act
        self.persistence.save_trade(trade)
        retrieved_trades = self.persistence.get_trades()
        
        # Assert
        assert len(retrieved_trades) == 1
        assert retrieved_trades[0].symbol == trade.symbol
        assert retrieved_trades[0].pnl == trade.pnl
    
    def test_settings_persistence(self):
        """Test settings saving and loading."""
        # Arrange
        settings = {
            "theme": "dark",
            "risk_pct": 0.02,
            "default_symbol": "EURUSD"
        }
        
        # Act
        self.persistence.save_settings(settings)
        loaded_settings = self.persistence.load_settings()
        
        # Assert
        assert loaded_settings["theme"] == "dark"
        assert loaded_settings["risk_pct"] == 0.02
        assert loaded_settings["default_symbol"] == "EURUSD"

Performance Testing

Benchmarking

# tests/performance/test_benchmarks.py
import pytest
import time
from forexsmartbot.services.backtest import BacktestService
from forexsmartbot.adapters.data import YFinanceProvider
from forexsmartbot.strategies import SMACrossoverStrategy

class TestPerformance:
    def test_backtest_performance(self, sample_data):
        """Test backtesting performance."""
        # Arrange
        data_provider = YFinanceProvider()
        backtest_service = BacktestService(data_provider)
        strategy = SMACrossoverStrategy()
        
        # Act
        start_time = time.time()
        results = backtest_service.run_backtest(
            strategy=strategy,
            symbol="EURUSD",
            start_date="2024-01-01",
            end_date="2024-01-31",
            initial_balance=10000.0
        )
        end_time = time.time()
        
        # Assert
        execution_time = end_time - start_time
        assert execution_time < 5.0  # Should complete in under 5 seconds
        assert 'error' not in results
    
    def test_memory_usage(self, sample_data):
        """Test memory usage during backtesting."""
        import psutil
        import os
        
        # Arrange
        process = psutil.Process(os.getpid())
        initial_memory = process.memory_info().rss
        
        # Act
        backtest_service = BacktestService(YFinanceProvider())
        strategy = SMACrossoverStrategy()
        results = backtest_service.run_backtest(
            strategy=strategy,
            symbol="EURUSD",
            start_date="2024-01-01",
            end_date="2024-01-31",
            initial_balance=10000.0
        )
        
        # Assert
        final_memory = process.memory_info().rss
        memory_used = (final_memory - initial_memory) / 1024 / 1024  # MB
        assert memory_used < 100  # Should use less than 100MB

Load Testing

# tests/performance/test_load.py
import pytest
import concurrent.futures
from forexsmartbot.core.risk_engine import RiskEngine, RiskConfig

class TestLoad:
    def test_concurrent_position_sizing(self):
        """Test concurrent position sizing calculations."""
        # Arrange
        engine = RiskEngine(RiskConfig())
        symbols = ["EURUSD", "GBPUSD", "USDJPY", "AUDUSD", "USDCAD"]
        
        def calculate_position_size(symbol):
            return engine.calculate_position_size(symbol, "SMA", 10000.0, 0.02)
        
        # Act
        start_time = time.time()
        with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
            futures = [executor.submit(calculate_position_size, symbol) for symbol in symbols]
            results = [future.result() for future in futures]
        end_time = time.time()
        
        # Assert
        execution_time = end_time - start_time
        assert execution_time < 1.0  # Should complete in under 1 second
        assert len(results) == len(symbols)
        assert all(result > 0 for result in results)
    
    def test_large_dataset_processing(self):
        """Test processing large datasets."""
        # Arrange
        import pandas as pd
        import numpy as np
        
        # Create large dataset
        dates = pd.date_range(start='2020-01-01', end='2024-01-01', freq='H')
        large_data = pd.DataFrame({
            'Open': np.random.uniform(1.0, 1.2, len(dates)),
            'High': np.random.uniform(1.0, 1.2, len(dates)),
            'Low': np.random.uniform(1.0, 1.2, len(dates)),
            'Close': np.random.uniform(1.0, 1.2, len(dates)),
            'Volume': np.random.randint(1000, 10000, len(dates))
        }, index=dates)
        
        strategy = SMACrossoverStrategy()
        
        # Act
        start_time = time.time()
        df = strategy.calculate_indicators(large_data)
        end_time = time.time()
        
        # Assert
        execution_time = end_time - start_time
        assert execution_time < 10.0  # Should complete in under 10 seconds
        assert len(df) == len(large_data)

Security Testing

Input Validation Testing

# tests/security/test_input_validation.py
import pytest
from forexsmartbot.core.risk_engine import RiskEngine, RiskConfig

class TestInputValidation:
    def test_malicious_input_handling(self):
        """Test handling of malicious input."""
        # Arrange
        engine = RiskEngine(RiskConfig())
        
        # Act & Assert
        with pytest.raises(ValueError):
            engine.calculate_position_size("'; DROP TABLE trades; --", "SMA", 10000.0, 0.02)
        
        with pytest.raises(ValueError):
            engine.calculate_position_size("EURUSD", "'; DROP TABLE trades; --", 10000.0, 0.02)
        
        with pytest.raises(ValueError):
            engine.calculate_position_size("EURUSD", "SMA", -10000.0, 0.02)
        
        with pytest.raises(ValueError):
            engine.calculate_position_size("EURUSD", "SMA", 10000.0, -0.02)
    
    def test_sql_injection_prevention(self):
        """Test SQL injection prevention."""
        # Arrange
        from forexsmartbot.services.persistence import PersistenceService
        persistence = PersistenceService(data_dir=":memory:")
        
        # Act & Assert
        with pytest.raises(ValueError):
            persistence.get_trades(symbol="'; DROP TABLE trades; --")
    
    def test_xss_prevention(self):
        """Test XSS prevention in UI components."""
        # Arrange
        malicious_input = "<script>alert('XSS')</script>"
        
        # Act
        sanitized = sanitize_input(malicious_input)
        
        # Assert
        assert "<script>" not in sanitized
        assert "alert" not in sanitized

Authentication Testing

# tests/security/test_authentication.py
import pytest
from forexsmartbot.core.security import APIKeyAuth

class TestAuthentication:
    def test_api_key_validation(self):
        """Test API key validation."""
        # Arrange
        auth = APIKeyAuth("secret_key")
        valid_message = "test_message"
        
        # Act
        signature = auth.generate_signature(valid_message)
        is_valid = auth.verify_signature(valid_message, signature)
        
        # Assert
        assert is_valid is True
    
    def test_invalid_api_key_rejection(self):
        """Test rejection of invalid API keys."""
        # Arrange
        auth = APIKeyAuth("secret_key")
        valid_message = "test_message"
        invalid_signature = "invalid_signature"
        
        # Act
        is_valid = auth.verify_signature(valid_message, invalid_signature)
        
        # Assert
        assert is_valid is False
    
    def test_brute_force_protection(self):
        """Test brute force protection."""
        # Arrange
        from forexsmartbot.core.security import BruteForceProtection
        protection = BruteForceProtection(max_attempts=3, lockout_duration=60)
        
        # Act
        for _ in range(3):
            protection.record_failed_attempt("user1")
        
        is_locked = protection.is_locked("user1")
        
        # Assert
        assert is_locked is True

UI Testing

PyQt6 Testing

# tests/ui/test_main_window.py
import pytest
from PyQt6.QtWidgets import QApplication
from forexsmartbot.ui.main_window import MainWindow

class TestMainWindow:
    def setup_method(self):
        """Set up UI test fixtures."""
        self.app = QApplication.instance()
        if self.app is None:
            self.app = QApplication([])
        self.window = MainWindow()
    
    def test_window_creation(self):
        """Test main window creation."""
        # Assert
        assert self.window is not None
        assert self.window.isVisible() is False
    
    def test_theme_switching(self):
        """Test theme switching functionality."""
        # Arrange
        initial_theme = self.window.get_current_theme()
        
        # Act
        self.window.set_theme("dark")
        dark_theme = self.window.get_current_theme()
        
        self.window.set_theme("light")
        light_theme = self.window.get_current_theme()
        
        # Assert
        assert dark_theme == "dark"
        assert light_theme == "light"
        assert dark_theme != light_theme
    
    def test_settings_dialog(self):
        """Test settings dialog functionality."""
        # Act
        self.window.show_settings_dialog()
        
        # Assert
        assert self.window.settings_dialog.isVisible() is True
    
    def test_log_display(self):
        """Test log display functionality."""
        # Arrange
        test_message = "Test log message"
        
        # Act
        self.window.append_log(test_message)
        
        # Assert
        log_content = self.window.log.toPlainText()
        assert test_message in log_content

Chart Testing

# tests/ui/test_charts.py
import pytest
import pandas as pd
from forexsmartbot.ui.charts import ChartWidget

class TestCharts:
    def setup_method(self):
        """Set up chart test fixtures."""
        self.chart_widget = ChartWidget()
        self.sample_data = pd.DataFrame({
            'Close': [1.1000, 1.1010, 1.1020, 1.1030, 1.1040],
            'SMA_10': [1.1005, 1.1015, 1.1025, 1.1035, 1.1045]
        })
    
    def test_chart_creation(self):
        """Test chart widget creation."""
        # Assert
        assert self.chart_widget is not None
        assert self.chart_widget.axes is not None
    
    def test_data_plotting(self):
        """Test data plotting functionality."""
        # Act
        self.chart_widget.plot_data(self.sample_data, "EURUSD")
        
        # Assert
        assert len(self.chart_widget.axes.lines) > 0
    
    def test_trade_markers(self):
        """Test trade marker functionality."""
        # Arrange
        trades = [
            {'entry_time': pd.Timestamp('2024-01-01'), 'entry_price': 1.1000, 'side': 1},
            {'entry_time': pd.Timestamp('2024-01-02'), 'entry_price': 1.1010, 'side': -1}
        ]
        
        # Act
        self.chart_widget.plot_data(self.sample_data, "EURUSD")
        self.chart_widget.add_trade_markers(trades)
        
        # Assert
        assert len(self.chart_widget.axes.collections) > 0

Test Automation

CI/CD Integration

# .github/workflows/test.yml
name: Tests

on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main ]

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        python-version: [3.11, 3.12]
    
    steps:
    - uses: actions/checkout@v4
    
    - name: Set up Python ${{ matrix.python-version }}
      uses: actions/setup-python@v4
      with:
        python-version: ${{ matrix.python-version }}
    
    - name: Install dependencies
      run: |
        python -m pip install --upgrade pip
        pip install -r requirements.txt
        pip install -r requirements-dev.txt
    
    - name: Run unit tests
      run: |
        python -m pytest tests/unit -v --cov=forexsmartbot --cov-report=xml
    
    - name: Run integration tests
      run: |
        python -m pytest tests/integration -v
    
    - name: Run security tests
      run: |
        python -m pytest tests/security -v
        bandit -r forexsmartbot/
        safety check
    
    - name: Upload coverage to Codecov
      uses: codecov/codecov-action@v3
      with:
        file: ./coverage.xml

Test Configuration

# pytest.ini
[tool:pytest]
testpaths = tests
python_files = test_*.py
python_classes = Test*
python_functions = test_*
addopts = 
    -v
    --tb=short
    --strict-markers
    --disable-warnings
    --cov=forexsmartbot
    --cov-report=term-missing
    --cov-report=html
    --cov-report=xml
markers =
    unit: Unit tests
    integration: Integration tests
    performance: Performance tests
    security: Security tests
    ui: UI tests
    slow: Slow tests

Test Data Management

# tests/fixtures/test_data.py
import pandas as pd
import numpy as np
from datetime import datetime, timedelta

class TestDataFactory:
    @staticmethod
    def create_sample_data(symbol="EURUSD", days=30):
        """Create sample market data for testing."""
        dates = pd.date_range(start='2024-01-01', periods=days, freq='D')
        base_price = 1.1000
        
        data = []
        for i, date in enumerate(dates):
            price = base_price + i * 0.0001 + np.random.normal(0, 0.001)
            data.append({
                'Date': date,
                'Open': price,
                'High': price + abs(np.random.normal(0, 0.002)),
                'Low': price - abs(np.random.normal(0, 0.002)),
                'Close': price + np.random.normal(0, 0.001),
                'Volume': np.random.randint(1000000, 5000000)
            })
        
        df = pd.DataFrame(data)
        df.set_index('Date', inplace=True)
        return df
    
    @staticmethod
    def create_trade_data(count=10):
        """Create sample trade data for testing."""
        trades = []
        for i in range(count):
            trades.append({
                'symbol': f'{"EURUSD" if i % 2 == 0 else "GBPUSD"}',
                'side': 1 if i % 3 == 0 else -1,
                'quantity': 100.0,
                'entry_price': 1.1000 + i * 0.0001,
                'exit_price': 1.1000 + i * 0.0001 + np.random.normal(0, 0.001),
                'pnl': np.random.normal(0, 50),
                'strategy': 'SMA_Crossover',
                'entry_time': datetime.now() - timedelta(days=i),
                'exit_time': datetime.now() - timedelta(days=i-1)
            })
        return trades

Best Practices

Test Organization

  1. Test Structure:

    tests/
    ├── unit/
    │   ├── test_risk_engine.py
    │   ├── test_strategies.py
    │   └── test_portfolio.py
    ├── integration/
    │   ├── test_trading_controller.py
    │   └── test_persistence.py
    ├── performance/
    │   ├── test_benchmarks.py
    │   └── test_load.py
    ├── security/
    │   ├── test_input_validation.py
    │   └── test_authentication.py
    ├── ui/
    │   ├── test_main_window.py
    │   └── test_charts.py
    └── fixtures/
        └── test_data.py
    
  2. Naming Conventions:

    • Test files: test_*.py
    • Test classes: Test*
    • Test methods: test_*
    • Fixtures: descriptive names

Test Quality

  1. Test Independence:

    • Each test should be independent
    • No shared state between tests
    • Clean up after each test
  2. Test Clarity:

    • Use descriptive test names
    • Follow Arrange-Act-Assert pattern
    • Add comments for complex tests
  3. Test Coverage:

    • Aim for 95%+ code coverage
    • Test all critical paths
    • Test edge cases and error conditions

Test Maintenance

  1. Regular Updates:

    • Update tests when code changes
    • Remove obsolete tests
    • Refactor duplicate test code
  2. Test Documentation:

    • Document test purpose
    • Explain complex test scenarios
    • Maintain test data documentation
  3. Performance Monitoring:

    • Monitor test execution time
    • Optimize slow tests
    • Use parallel execution where possible

Conclusion

Comprehensive testing is essential for ForexSmartBot to ensure:

  1. Reliability: Tests catch bugs before production
  2. Quality: High test coverage ensures code quality
  3. Confidence: Tests provide confidence in changes
  4. Documentation: Tests serve as living documentation
  5. Maintainability: Well-tested code is easier to maintain

Remember that testing is an investment in code quality and should be treated as a first-class citizen in the development process.


Note: This guide provides comprehensive testing strategies. Adapt the approaches to your specific testing needs and constraints.