This guide covers comprehensive testing strategies for ForexSmartBot, including unit tests, integration tests, performance tests, and security tests.
- Testing Overview
- Test Types
- Unit Testing
- Integration Testing
- Performance Testing
- Security Testing
- UI Testing
- Test Automation
- Test Data Management
- Best Practices
- Test-Driven Development: Write tests before code
- Comprehensive Coverage: Test all critical paths
- Automated Testing: Automate as much as possible
- Continuous Testing: Test continuously in CI/CD
- Quality Assurance: Ensure high quality standards
/\
/ \
/ UI \ (Few, slow, expensive)
/______\
/ \
/Integration\ (Some, medium speed, cost)
/____________\
/ \
/ Unit Tests \ (Many, fast, cheap)
/________________\
- 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
- 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
- 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
- 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
- Purpose: Test security vulnerabilities
- Scope: Security-sensitive components
- Speed: Medium to slow
- Frequency: Run weekly or on demand
- Tools: bandit, safety, semgrep
- 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
# 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# 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},
]# 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# 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# 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"# 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# 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)# 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# 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# 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# 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# .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# 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# 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-
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 -
Naming Conventions:
- Test files:
test_*.py - Test classes:
Test* - Test methods:
test_* - Fixtures: descriptive names
- Test files:
-
Test Independence:
- Each test should be independent
- No shared state between tests
- Clean up after each test
-
Test Clarity:
- Use descriptive test names
- Follow Arrange-Act-Assert pattern
- Add comments for complex tests
-
Test Coverage:
- Aim for 95%+ code coverage
- Test all critical paths
- Test edge cases and error conditions
-
Regular Updates:
- Update tests when code changes
- Remove obsolete tests
- Refactor duplicate test code
-
Test Documentation:
- Document test purpose
- Explain complex test scenarios
- Maintain test data documentation
-
Performance Monitoring:
- Monitor test execution time
- Optimize slow tests
- Use parallel execution where possible
Comprehensive testing is essential for ForexSmartBot to ensure:
- Reliability: Tests catch bugs before production
- Quality: High test coverage ensures code quality
- Confidence: Tests provide confidence in changes
- Documentation: Tests serve as living documentation
- 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.